Explorar el Código

价目表和分类表拆分

lutong hace 2 semanas
padre
commit
3770d5b10d
Se han modificado 16 ficheros con 363 adiciones y 97 borrados
  1. 17 12
      alien-dining/src/main/java/shop/alien/dining/controller/StoreInfoController.java
  2. 15 11
      alien-dining/src/main/java/shop/alien/dining/service/StoreInfoService.java
  3. 132 16
      alien-dining/src/main/java/shop/alien/dining/service/impl/StoreInfoServiceImpl.java
  4. 4 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreCuisineCategory.java
  5. 38 0
      alien-entity/src/main/java/shop/alien/entity/store/constants/StoreMenuType.java
  6. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreCuisineCategoryDTO.java
  7. 35 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/CategoryMenuGroupVO.java
  8. 2 6
      alien-entity/src/main/java/shop/alien/entity/store/vo/CategoryWithCuisinesVO.java
  9. 27 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StorePriceWithPricesVO.java
  10. 3 0
      alien-entity/src/main/resources/db/migration/store_cuisine_category_menu_type.sql
  11. 8 7
      alien-store/src/main/java/shop/alien/store/controller/DiningServiceController.java
  12. 10 6
      alien-store/src/main/java/shop/alien/store/controller/StoreCuisineCategoryController.java
  13. 11 8
      alien-store/src/main/java/shop/alien/store/controller/dining/StoreInfoDiningPathProxyController.java
  14. 7 8
      alien-store/src/main/java/shop/alien/store/feign/DiningServiceFeign.java
  15. 20 10
      alien-store/src/main/java/shop/alien/store/service/StoreCuisineCategoryService.java
  16. 31 13
      alien-store/src/main/java/shop/alien/store/service/impl/StoreCuisineCategoryServiceImpl.java

+ 17 - 12
alien-dining/src/main/java/shop/alien/dining/controller/StoreInfoController.java

@@ -9,7 +9,7 @@ import shop.alien.entity.store.StoreCuisine;
 import shop.alien.entity.store.StoreCuisineCategory;
 import shop.alien.entity.store.StoreTable;
 import shop.alien.entity.store.dto.StoreInfoWithHomepageCuisinesDTO;
-import shop.alien.entity.store.vo.CategoryWithCuisinesVO;
+import shop.alien.entity.store.vo.CategoryMenuGroupVO;
 import shop.alien.dining.service.StoreInfoService;
 
 import java.util.List;
@@ -47,16 +47,17 @@ public class StoreInfoController {
         }
     }
 
-    @ApiOperation(value = "根据门店ID查询菜品种类列表", notes = "查询指定门店下的所有菜品种类")
+    @ApiOperation(value = "根据门店ID查询分类列表", notes = "默认美食分类;传 menuType=2 可拉取通用价目分类(与 store_price.category_ids 对应)")
     @GetMapping("/categories")
     public R<List<StoreCuisineCategory>> getCategoriesByStoreId(
-            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId) {
-        log.info("StoreInfoController.getCategoriesByStoreId?storeId={}", storeId);
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId,
+            @ApiParam(value = "可选 1=美食 2=通用价目,默认 1") @RequestParam(required = false) Integer menuType) {
+        log.info("StoreInfoController.getCategoriesByStoreId?storeId={}, menuType={}", storeId, menuType);
         try {
             if (storeId == null) {
                 return R.fail("门店ID不能为空");
             }
-            List<StoreCuisineCategory> categories = storeInfoService.getCategoriesByStoreId(storeId);
+            List<StoreCuisineCategory> categories = storeInfoService.getCategoriesByStoreId(storeId, menuType);
             return R.data(categories);
         } catch (Exception e) {
             log.error("查询菜品种类列表失败: {}", e.getMessage(), e);
@@ -64,21 +65,25 @@ public class StoreInfoController {
         }
     }
 
-    @ApiOperation(value = "根据门店ID查询菜品种类及各类别下菜品", notes = "一次返回所有菜品种类及每个分类下的菜品列表;可选 keyword 按菜品名称模糊查询。category 与 /store/info/categories 单条一致;cuisines 每项为 StoreCuisine 全字段,并含 originalPrice、currentPrice、hasActiveDiscount(按 store_product_discount_rule 当前时刻命中,与门店优惠配置一致)。")
+    @ApiOperation(value = "分类树(美食或通用价目)", notes = "同一接口:menuType=1(默认)返回 category+cuisines;menuType=2 返回 category+prices(store_price)。keyword 可选,模糊名称。空侧字段不序列化,默认与历史美食 JSON 接近。")
     @GetMapping("/categories-with-cuisines")
-    public R<List<CategoryWithCuisinesVO>> getCategoriesWithCuisinesByStoreId(
+    public R<List<CategoryMenuGroupVO>> getCategoriesWithMenuGroupsByStoreId(
             @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId,
-            @ApiParam(value = "菜品名称模糊查询关键词(可选)") @RequestParam(required = false) String keyword) {
-        log.info("StoreInfoController.getCategoriesWithCuisinesByStoreId?storeId={}, keyword={}", storeId, keyword);
+            @ApiParam(value = "名称模糊查询(可选)") @RequestParam(required = false) String keyword,
+            @ApiParam(value = "1=美食 2=通用价目,默认 1") @RequestParam(required = false) Integer menuType) {
+        log.info("StoreInfoController.getCategoriesWithMenuGroupsByStoreId?storeId={}, keyword={}, menuType={}",
+                storeId, keyword, menuType);
         try {
             if (storeId == null) {
                 return R.fail("门店ID不能为空");
             }
-            List<CategoryWithCuisinesVO> list = storeInfoService.getCategoriesWithCuisinesByStoreId(storeId, keyword);
+            List<CategoryMenuGroupVO> list = storeInfoService.getCategoriesWithMenuGroupsByStoreId(storeId, keyword, menuType);
             return R.data(list);
+        } catch (IllegalArgumentException e) {
+            return R.fail(e.getMessage());
         } catch (Exception e) {
-            log.error("查询菜品种类及菜品失败: {}", e.getMessage(), e);
-            return R.fail("查询菜品种类及菜品失败: " + e.getMessage());
+            log.error("查询分类树失败: {}", e.getMessage(), e);
+            return R.fail("查询分类树失败: " + e.getMessage());
         }
     }
 

+ 15 - 11
alien-dining/src/main/java/shop/alien/dining/service/StoreInfoService.java

@@ -3,8 +3,9 @@ package shop.alien.dining.service;
 import shop.alien.entity.store.StoreCuisine;
 import shop.alien.entity.store.StoreCuisineCategory;
 import shop.alien.entity.store.StoreTable;
+import shop.alien.entity.store.constants.StoreMenuType;
 import shop.alien.entity.store.dto.StoreInfoWithHomepageCuisinesDTO;
-import shop.alien.entity.store.vo.CategoryWithCuisinesVO;
+import shop.alien.entity.store.vo.CategoryMenuGroupVO;
 
 import java.util.List;
 
@@ -25,12 +26,16 @@ public interface StoreInfoService {
     List<StoreTable> getTablesByStoreId(Integer storeId);
 
     /**
-     * 根据门店ID查询菜品种类列表
-     *
-     * @param storeId 门店ID
-     * @return 菜品种类列表
+     * 查询分类列表(默认仅美食分类,与历史一致)
+     */
+    default List<StoreCuisineCategory> getCategoriesByStoreId(Integer storeId) {
+        return getCategoriesByStoreId(storeId, StoreMenuType.CUISINE);
+    }
+
+    /**
+     * @param menuType {@link StoreMenuType},null 视作美食
      */
-    List<StoreCuisineCategory> getCategoriesByStoreId(Integer storeId);
+    List<StoreCuisineCategory> getCategoriesByStoreId(Integer storeId, Integer menuType);
 
     /**
      * 根据菜品种类ID查询菜品信息列表
@@ -41,13 +46,12 @@ public interface StoreInfoService {
     List<StoreCuisine> getCuisinesByCategoryId(Integer categoryId);
 
     /**
-     * 根据门店ID查询菜品种类及每个分类下的菜品列表(一次返回种类+菜品)
+     * 分类树:{@code menuType=1}(默认)美食 + store_cuisine;{@code menuType=2} 通用价目 + store_price。
+     * 路径统一为 GET /store/info/categories-with-cuisines。
      *
-     * @param storeId 门店ID
-     * @param keyword 菜品名称模糊查询关键词(可选,为空则不按名称筛选)
-     * @return 菜品种类及下属菜品列表
+     * @param menuType {@link shop.alien.entity.store.constants.StoreMenuType},null 视为 1
      */
-    List<CategoryWithCuisinesVO> getCategoriesWithCuisinesByStoreId(Integer storeId, String keyword);
+    List<CategoryMenuGroupVO> getCategoriesWithMenuGroupsByStoreId(Integer storeId, String keyword, Integer menuType);
 
     /**
      * 删除菜品种类:仅逻辑删除分类并解除菜品与该分类的绑定关系,价目表(菜品)本身不改动

+ 132 - 16
alien-dining/src/main/java/shop/alien/dining/service/impl/StoreInfoServiceImpl.java

@@ -9,16 +9,21 @@ import org.springframework.stereotype.Service;
 import shop.alien.entity.store.StoreCuisine;
 import shop.alien.entity.store.StoreCuisineCategory;
 import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.StorePrice;
+import shop.alien.entity.store.constants.StoreMenuType;
 import shop.alien.entity.store.StoreProductDiscountRule;
 import shop.alien.entity.store.StoreTable;
 import shop.alien.entity.store.dto.StoreInfoWithHomepageCuisinesDTO;
-import shop.alien.entity.store.vo.CategoryWithCuisinesVO;
+import shop.alien.entity.store.vo.CategoryMenuGroupVO;
 import shop.alien.entity.store.vo.StoreCuisineWithPricesVO;
+import shop.alien.entity.store.vo.StorePriceWithPricesVO;
 import shop.alien.mapper.StoreCuisineCategoryMapper;
 import shop.alien.mapper.StoreCuisineMapper;
 import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.mapper.StorePriceMapper;
 import shop.alien.mapper.StoreProductDiscountRuleMapper;
 import shop.alien.mapper.StoreTableMapper;
+import shop.alien.dining.constants.OrderMenuConstants;
 import shop.alien.dining.service.StoreInfoService;
 import shop.alien.dining.support.ProductDiscountPricingSupport;
 import org.apache.commons.lang3.StringUtils;
@@ -48,6 +53,7 @@ public class StoreInfoServiceImpl implements StoreInfoService {
     private final StoreTableMapper storeTableMapper;
     private final StoreCuisineCategoryMapper storeCuisineCategoryMapper;
     private final StoreCuisineMapper storeCuisineMapper;
+    private final StorePriceMapper storePriceMapper;
     private final StoreInfoMapper storeInfoMapper;
     private final StoreProductDiscountRuleMapper storeProductDiscountRuleMapper;
 
@@ -64,15 +70,17 @@ public class StoreInfoServiceImpl implements StoreInfoService {
     }
 
     @Override
-    public List<StoreCuisineCategory> getCategoriesByStoreId(Integer storeId) {
-        log.info("根据门店ID查询菜品种类列表, storeId={}", storeId);
-        
+    public List<StoreCuisineCategory> getCategoriesByStoreId(Integer storeId, Integer menuType) {
+        int mt = StoreMenuType.normalizeOrCuisine(menuType);
+        log.info("根据门店ID查询分类列表, storeId={}, menuType={}", storeId, mt);
+
         LambdaQueryWrapper<StoreCuisineCategory> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(StoreCuisineCategory::getStoreId, storeId);
+        wrapper.eq(StoreCuisineCategory::getMenuType, mt);
         wrapper.eq(StoreCuisineCategory::getDeleteFlag, 0);
-        wrapper.eq(StoreCuisineCategory::getStatus, 1); // 只查询启用的分类
-        wrapper.orderByAsc(StoreCuisineCategory::getSort); // 按排序字段排序
-        
+        wrapper.eq(StoreCuisineCategory::getStatus, 1);
+        wrapper.orderByAsc(StoreCuisineCategory::getSort);
+
         return storeCuisineCategoryMapper.selectList(wrapper);
     }
 
@@ -86,8 +94,12 @@ public class StoreInfoServiceImpl implements StoreInfoService {
             log.warn("菜品种类不存在, categoryId={}", categoryId);
             return new java.util.ArrayList<>();
         }
-        
-        // 查询该门店下所有上架的菜品(与 getCategoriesWithCuisinesByStoreId 中 cuisines 查询条件、排序一致)
+        if (!StoreMenuType.isCuisine(category.getMenuType())) {
+            log.warn("该分类非美食分类,不返回 store_cuisine 列表, categoryId={}, menuType={}", categoryId, category.getMenuType());
+            return new java.util.ArrayList<>();
+        }
+
+        // 查询该门店下所有上架的菜品(与 getCategoriesWithMenuGroupsByStoreId(menuType=1) 中 cuisines 查询条件、排序一致)
         LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(StoreCuisine::getStoreId, category.getStoreId());
         wrapper.eq(StoreCuisine::getDeleteFlag, 0);
@@ -119,13 +131,20 @@ public class StoreInfoServiceImpl implements StoreInfoService {
     }
 
     @Override
-    public List<CategoryWithCuisinesVO> getCategoriesWithCuisinesByStoreId(Integer storeId, String keyword) {
-        log.info("根据门店ID查询菜品种类及下属菜品, storeId={}, keyword={}", storeId, keyword);
+    public List<CategoryMenuGroupVO> getCategoriesWithMenuGroupsByStoreId(Integer storeId, String keyword, Integer menuType) {
+        int mt = StoreMenuType.normalizeOrCuisine(menuType);
+        log.info("根据门店ID查询分类树, storeId={}, keyword={}, menuType={}", storeId, keyword, mt);
+        if (mt == StoreMenuType.GENERIC_PRICE) {
+            return buildGenericCategoryMenuGroups(storeId, keyword);
+        }
+        return buildCuisineCategoryMenuGroups(storeId, keyword);
+    }
+
+    private List<CategoryMenuGroupVO> buildCuisineCategoryMenuGroups(Integer storeId, String keyword) {
         List<StoreCuisineCategory> categories = getCategoriesByStoreId(storeId);
         if (categories == null || categories.isEmpty()) {
             return new ArrayList<>();
         }
-        // 与 getCuisinesByCategoryId 相同的查询条件与排序,保证 cuisines 字段与 /store/info/cuisines 一致
         LambdaQueryWrapper<StoreCuisine> cuisineWrapper = new LambdaQueryWrapper<>();
         cuisineWrapper.eq(StoreCuisine::getStoreId, storeId);
         cuisineWrapper.eq(StoreCuisine::getDeleteFlag, 0);
@@ -148,7 +167,7 @@ public class StoreInfoServiceImpl implements StoreInfoService {
         }
         Map<Integer, BigDecimal> discountedByProductId = buildDiscountedPriceMap(storeId, flatForPricing);
 
-        List<CategoryWithCuisinesVO> result = new ArrayList<>();
+        List<CategoryMenuGroupVO> result = new ArrayList<>();
         for (StoreCuisineCategory category : categories) {
             Integer categoryId = category.getId();
             List<StoreCuisineWithPricesVO> cuisines = allCuisines.stream()
@@ -156,7 +175,47 @@ public class StoreInfoServiceImpl implements StoreInfoService {
                     .filter(c -> !filterByName || (c.getName() != null && c.getName().toLowerCase().contains(keywordLower)))
                     .map(c -> toCuisineWithPricesVo(c, discountedByProductId))
                     .collect(Collectors.toList());
-            result.add(new CategoryWithCuisinesVO(category, cuisines));
+            result.add(new CategoryMenuGroupVO(category, cuisines, null));
+        }
+        return result;
+    }
+
+    private List<CategoryMenuGroupVO> buildGenericCategoryMenuGroups(Integer storeId, String keyword) {
+        List<StoreCuisineCategory> categories = getCategoriesByStoreId(storeId, StoreMenuType.GENERIC_PRICE);
+        if (categories == null || categories.isEmpty()) {
+            return new ArrayList<>();
+        }
+        LambdaQueryWrapper<StorePrice> priceWrapper = new LambdaQueryWrapper<>();
+        priceWrapper.eq(StorePrice::getStoreId, storeId);
+        priceWrapper.eq(StorePrice::getShelfStatus, 1);
+        priceWrapper.eq(StorePrice::getStatus, 1);
+        priceWrapper.orderByAsc(StorePrice::getId);
+        List<StorePrice> allPrices = storePriceMapper.selectList(priceWrapper);
+        if (allPrices == null) {
+            allPrices = new ArrayList<>();
+        }
+        boolean filterByName = StringUtils.isNotBlank(keyword);
+        String keywordLower = filterByName ? keyword.trim().toLowerCase() : null;
+
+        List<StorePrice> flatForPricing = new ArrayList<>();
+        for (StoreCuisineCategory category : categories) {
+            Integer categoryId = category.getId();
+            allPrices.stream()
+                    .filter(p -> priceBelongsToCategory(p, categoryId))
+                    .filter(p -> !filterByName || (p.getName() != null && p.getName().toLowerCase().contains(keywordLower)))
+                    .forEach(flatForPricing::add);
+        }
+        Map<Integer, BigDecimal> discountedByProductId = buildDiscountedPriceMapForGeneric(storeId, flatForPricing);
+
+        List<CategoryMenuGroupVO> result = new ArrayList<>();
+        for (StoreCuisineCategory category : categories) {
+            Integer categoryId = category.getId();
+            List<StorePriceWithPricesVO> prices = allPrices.stream()
+                    .filter(p -> priceBelongsToCategory(p, categoryId))
+                    .filter(p -> !filterByName || (p.getName() != null && p.getName().toLowerCase().contains(keywordLower)))
+                    .map(p -> toPriceWithPricesVo(p, discountedByProductId))
+                    .collect(Collectors.toList());
+            result.add(new CategoryMenuGroupVO(category, null, prices));
         }
         return result;
     }
@@ -195,6 +254,39 @@ public class StoreInfoServiceImpl implements StoreInfoService {
                 rules);
     }
 
+    /** 通用价目:仅 rule_product_type=2 的规则参与计价 */
+    private Map<Integer, BigDecimal> buildDiscountedPriceMapForGeneric(Integer storeId, List<StorePrice> visiblePrices) {
+        if (visiblePrices == null || visiblePrices.isEmpty()) {
+            return new HashMap<>();
+        }
+        Set<Integer> idSet = new HashSet<>();
+        Map<Integer, BigDecimal> basePriceByProduct = new HashMap<>();
+        for (StorePrice p : visiblePrices) {
+            if (p.getId() == null) {
+                continue;
+            }
+            if (idSet.add(p.getId())) {
+                BigDecimal base = p.getTotalPrice() != null ? p.getTotalPrice() : BigDecimal.ZERO;
+                basePriceByProduct.put(p.getId(), base);
+            }
+        }
+        List<Integer> productIds = new ArrayList<>(idSet);
+        if (productIds.isEmpty()) {
+            return new HashMap<>();
+        }
+        List<StoreProductDiscountRule> rules = storeProductDiscountRuleMapper.selectList(
+                new LambdaQueryWrapper<StoreProductDiscountRule>()
+                        .eq(StoreProductDiscountRule::getStoreId, storeId)
+                        .eq(StoreProductDiscountRule::getStatus, 1)
+                        .eq(StoreProductDiscountRule::getRuleProductType, OrderMenuConstants.RULE_PRODUCT_GENERIC_PRICE)
+                        .in(StoreProductDiscountRule::getProductId, productIds));
+        return ProductDiscountPricingSupport.resolveDiscountedPrices(
+                productIds,
+                basePriceByProduct,
+                LocalDateTime.now(),
+                rules);
+    }
+
     private static StoreCuisineWithPricesVO toCuisineWithPricesVo(StoreCuisine cuisine, Map<Integer, BigDecimal> discountedByProductId) {
         Map<Integer, BigDecimal> disc = discountedByProductId == null ? Collections.emptyMap() : discountedByProductId;
         StoreCuisineWithPricesVO vo = new StoreCuisineWithPricesVO();
@@ -208,9 +300,29 @@ public class StoreInfoServiceImpl implements StoreInfoService {
         return vo;
     }
 
+    private static StorePriceWithPricesVO toPriceWithPricesVo(StorePrice price, Map<Integer, BigDecimal> discountedByProductId) {
+        Map<Integer, BigDecimal> disc = discountedByProductId == null ? Collections.emptyMap() : discountedByProductId;
+        StorePriceWithPricesVO vo = new StorePriceWithPricesVO();
+        BeanUtils.copyProperties(price, vo);
+        BigDecimal original = price.getTotalPrice() != null ? price.getTotalPrice() : BigDecimal.ZERO;
+        vo.setOriginalPrice(original);
+        boolean hasRule = price.getId() != null && disc.containsKey(price.getId());
+        BigDecimal current = hasRule ? disc.get(price.getId()) : original;
+        vo.setCurrentPrice(current);
+        vo.setHasActiveDiscount(hasRule);
+        return vo;
+    }
+
     private boolean belongsToCategory(StoreCuisine cuisine, Integer categoryId) {
-        String categoryIdsStr = cuisine.getCategoryIds();
-        if (StringUtils.isBlank(categoryIdsStr)) {
+        return categoryJsonContains(cuisine.getCategoryIds(), categoryId);
+    }
+
+    private boolean priceBelongsToCategory(StorePrice price, Integer categoryId) {
+        return categoryJsonContains(price.getCategoryIds(), categoryId);
+    }
+
+    private static boolean categoryJsonContains(String categoryIdsStr, Integer categoryId) {
+        if (StringUtils.isBlank(categoryIdsStr) || categoryId == null) {
             return false;
         }
         try {
@@ -233,6 +345,10 @@ public class StoreInfoServiceImpl implements StoreInfoService {
             log.warn("删除菜品种类失败:分类不存在, categoryId={}", categoryId);
             return false;
         }
+        if (!StoreMenuType.isCuisine(category.getMenuType())) {
+            log.warn("删除菜品种类失败:仅支持美食分类,通用价目分类请在商家端维护, categoryId={}", categoryId);
+            return false;
+        }
         Integer storeId = category.getStoreId();
         // 1. 解除绑定:将该分类ID从所有关联菜品的 category_ids 中移除,价目表菜品其它字段不变
         LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();

+ 4 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreCuisineCategory.java

@@ -29,6 +29,10 @@ public class StoreCuisineCategory {
     @TableField("store_id")
     private Integer storeId;
 
+    @ApiModelProperty(value = "分类归属(1:美食/store_cuisine,2:通用价目/store_price),见 shop.alien.entity.store.constants.StoreMenuType")
+    @TableField("menu_type")
+    private Integer menuType;
+
     @ApiModelProperty(value = "分类名称")
     @TableField("category_name")
     private String categoryName;

+ 38 - 0
alien-entity/src/main/java/shop/alien/entity/store/constants/StoreMenuType.java

@@ -0,0 +1,38 @@
+package shop.alien.entity.store.constants;
+
+/**
+ * 门店侧「菜单维度」:与 {@code store_order.menu_type}、桌台点餐类型等一致。
+ * <p>
+ * 用于 {@code store_cuisine_category.menu_type} 等字段。
+ */
+public final class StoreMenuType {
+
+    private StoreMenuType() {
+    }
+
+    /** 美食 / store_cuisine */
+    public static final int CUISINE = 1;
+    /** 通用价目 / store_price */
+    public static final int GENERIC_PRICE = 2;
+
+    /**
+     * 规范化入参:null 视作美食(兼容旧调用未传类型)。
+     */
+    public static int normalizeOrCuisine(Integer menuType) {
+        if (menuType == null) {
+            return CUISINE;
+        }
+        if (menuType == CUISINE || menuType == GENERIC_PRICE) {
+            return menuType;
+        }
+        throw new IllegalArgumentException("menuType 仅支持 1=美食 2=通用价目,当前值=" + menuType);
+    }
+
+    public static boolean isCuisine(Integer menuType) {
+        return menuType == null || menuType == CUISINE;
+    }
+
+    public static boolean isGenericPrice(Integer menuType) {
+        return menuType != null && menuType == GENERIC_PRICE;
+    }
+}

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreCuisineCategoryDTO.java

@@ -29,4 +29,7 @@ public class StoreCuisineCategoryDTO {
     @ApiModelProperty(value = "分类名称列表", notes = "批量创建分类时必填,多个分类名称用英文逗号分隔,如:热菜,水果,甜品")
     private String categoryNames;
 
+    @ApiModelProperty(value = "分类归属", notes = "1=美食 2=通用价目,默认 1;批量创建时写入各新行")
+    private Integer menuType;
+
 }

+ 35 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CategoryMenuGroupVO.java

@@ -0,0 +1,35 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import shop.alien.entity.store.StoreCuisineCategory;
+
+import java.util.List;
+
+/**
+ * 门店分类树统一项:由 {@code GET /store/info/categories-with-cuisines} 的 {@code menuType} 决定填充哪一侧。
+ * <p>
+ * {@code menuType=1}(默认)仅填充 {@link #cuisines};{@code menuType=2} 仅填充 {@link #prices}。
+ * 空列表字段默认不参与序列化,便于与历史仅含 category+cuisines 的响应对齐。
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ApiModel(value = "CategoryMenuGroupVO", description = "分类 + 美食菜品或通用价目项(由 menuType 决定哪侧有数据)")
+public class CategoryMenuGroupVO {
+
+    @ApiModelProperty(value = "分类信息(menu_type 与请求的 menuType 一致)")
+    private StoreCuisineCategory category;
+
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @ApiModelProperty(value = "美食子项(menuType=1)")
+    private List<StoreCuisineWithPricesVO> cuisines;
+
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @ApiModelProperty(value = "通用价目子项(menuType=2)")
+    private List<StorePriceWithPricesVO> prices;
+}

+ 2 - 6
alien-entity/src/main/java/shop/alien/entity/store/vo/CategoryWithCuisinesVO.java

@@ -10,12 +10,8 @@ import shop.alien.entity.store.StoreCuisineCategory;
 import java.util.List;
 
 /**
- * 菜品种类及其下属菜品 VO(通过门店ID查询时返回)
- * <p>
- * 与单独接口的返回结构一致:
- * - category 与 GET /store/info/categories 中每个元素的字段完全一致(StoreCuisineCategory)
- * - cuisines 在 StoreCuisine 字段基础上增加 originalPrice、currentPrice、hasActiveDiscount(与菜品优惠规则展示一致)
- * </p>
+ * 历史结构:仅美食分类 + 菜品列表。
+ * 新接口统一返回 {@link CategoryMenuGroupVO},通过 query {@code menuType} 区分美食/通用;此类仍保留供兼容与文档参照。
  *
  * @author system
  */

+ 27 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StorePriceWithPricesVO.java

@@ -0,0 +1,27 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import shop.alien.entity.store.StorePrice;
+
+import java.math.BigDecimal;
+
+/**
+ * 通用价目表行 + 展示用原价/现价(与 {@link shop.alien.entity.store.StoreProductDiscountRule} 且 {@code rule_product_type=2} 命中逻辑对齐)
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ApiModel(value = "StorePriceWithPricesVO", description = "通用价目项(含原价、现价);其它字段与 StorePrice 一致")
+public class StorePriceWithPricesVO extends StorePrice {
+
+    @ApiModelProperty(value = "原价(门店标价,与 totalPrice 一致)")
+    private BigDecimal originalPrice;
+
+    @ApiModelProperty(value = "现价(按通用价目优惠规则当前时刻命中后的价格;无优惠时等于原价)")
+    private BigDecimal currentPrice;
+
+    @ApiModelProperty(value = "当前是否存在生效中的价目优惠")
+    private Boolean hasActiveDiscount;
+}

+ 3 - 0
alien-entity/src/main/resources/db/migration/store_cuisine_category_menu_type.sql

@@ -0,0 +1,3 @@
+-- 菜品/价目分类表:区分美食分类与通用价目分类(与订单 menu_type 语义一致)
+ALTER TABLE `store_cuisine_category`
+    ADD COLUMN `menu_type` TINYINT NOT NULL DEFAULT 1 COMMENT '分类归属:1=美食(store_cuisine) 2=通用价目(store_price)' AFTER `store_id`;

+ 8 - 7
alien-store/src/main/java/shop/alien/store/controller/DiningServiceController.java

@@ -326,18 +326,19 @@ public class DiningServiceController {
         }
     }
 
-    @ApiOperation(value = "根据门店ID查询菜品种类及各类别下菜品", notes = "一次返回所有菜品种类及每个分类下的菜品列表;可选 keyword 按菜品名称模糊查询")
+    @ApiOperation(value = "门店分类树(美食或通用价目)", notes = "与 alien-dining 同名接口;menuType=1(默认)cuisines;menuType=2 prices;可选 keyword")
     @ApiOperationSupport(order = 11)
     @GetMapping("/store/info/categories-with-cuisines")
-    public R<List<CategoryWithCuisinesVO>> getCategoriesWithCuisinesByStoreId(
+    public R<List<CategoryMenuGroupVO>> getCategoriesWithMenuGroupsByStoreId(
             @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId,
-            @ApiParam(value = "菜品名称模糊查询关键词(可选)") @RequestParam(required = false) String keyword) {
+            @ApiParam(value = "名称模糊查询关键词(可选)") @RequestParam(required = false) String keyword,
+            @ApiParam(value = "1=美食 2=通用价目,默认 1") @RequestParam(required = false) Integer menuType) {
         try {
-            log.info("根据门店ID查询菜品种类及菜品: storeId={}, keyword={}", storeId, keyword);
-            return diningServiceFeign.getCategoriesWithCuisinesByStoreId(storeId, keyword);
+            log.info("门店分类树: storeId={}, keyword={}, menuType={}", storeId, keyword, menuType);
+            return diningServiceFeign.getCategoriesWithMenuGroupsByStoreId(storeId, keyword, menuType);
         } catch (Exception e) {
-            log.error("查询菜品种类及菜品失败: {}", e.getMessage(), e);
-            return R.fail("查询菜品种类及菜品失败: " + e.getMessage());
+            log.error("查询门店分类树失败: {}", e.getMessage(), e);
+            return R.fail("查询门店分类树失败: " + e.getMessage());
         }
     }
 

+ 10 - 6
alien-store/src/main/java/shop/alien/store/controller/StoreCuisineCategoryController.java

@@ -33,14 +33,17 @@ public class StoreCuisineCategoryController {
     private final StoreCuisineCategoryService storeCuisineCategoryService;
 
     @ApiOperationSupport(order = 1)
-    @ApiOperation("查询菜品分类列表")
+    @ApiOperation("查询菜品分类列表(可按 menuType 区分美食 / 通用价目分类)")
     @ApiImplicitParams({
-            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true)
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "menuType", value = "可选,1=美食 2=通用价目,默认 1", dataType = "Integer", paramType = "query", required = false)
     })
     @GetMapping("/getCategoryList")
-    public R<List<StoreCuisineCategory>> getCategoryList(@RequestParam Integer storeId) {
-        log.info("StoreCuisineCategoryController.getCategoryList?storeId={}", storeId);
-        return R.data(storeCuisineCategoryService.getCategoryList(storeId));
+    public R<List<StoreCuisineCategory>> getCategoryList(
+            @RequestParam Integer storeId,
+            @RequestParam(required = false) Integer menuType) {
+        log.info("StoreCuisineCategoryController.getCategoryList?storeId={}&menuType={}", storeId, menuType);
+        return R.data(storeCuisineCategoryService.getCategoryList(storeId, menuType));
     }
 
     @ApiOperationSupport(order = 3)
@@ -65,7 +68,8 @@ public class StoreCuisineCategoryController {
         }
 
         try {
-            boolean result = storeCuisineCategoryService.batchCreateCategories(dto.getStoreId(), categoryNameList);
+            boolean result = storeCuisineCategoryService.batchCreateCategories(
+                    dto.getStoreId(), categoryNameList, dto.getMenuType());
             if (result) {
                 return R.success("批量创建菜品分类成功");
             } else {

+ 11 - 8
alien-store/src/main/java/shop/alien/store/controller/dining/StoreInfoDiningPathProxyController.java

@@ -10,7 +10,7 @@ import shop.alien.entity.store.StoreCuisine;
 import shop.alien.entity.store.StoreCuisineCategory;
 import shop.alien.entity.store.StoreTable;
 import shop.alien.entity.store.dto.StoreInfoWithHomepageCuisinesDTO;
-import shop.alien.entity.store.vo.CategoryWithCuisinesVO;
+import shop.alien.entity.store.vo.CategoryMenuGroupVO;
 import shop.alien.store.feign.DiningServiceFeign;
 
 import java.util.List;
@@ -39,11 +39,13 @@ public class StoreInfoDiningPathProxyController {
         }
     }
 
-    @ApiOperation("菜品种类列表(与小程序 GetStoreCategories 一致)")
+    @ApiOperation("分类列表(与小程序一致;可选 menuType=2 拉通用价目分类)")
     @GetMapping("/categories")
-    public R<List<StoreCuisineCategory>> getCategories(@RequestParam Integer storeId) {
+    public R<List<StoreCuisineCategory>> getCategories(
+            @RequestParam Integer storeId,
+            @RequestParam(required = false) Integer menuType) {
         try {
-            return diningServiceFeign.getStoreInfoCategories(storeId);
+            return diningServiceFeign.getStoreInfoCategories(storeId, menuType);
         } catch (Exception e) {
             log.error("store/info/categories: {}", e.getMessage(), e);
             return R.fail(e.getMessage());
@@ -72,13 +74,14 @@ public class StoreInfoDiningPathProxyController {
         }
     }
 
-    @ApiOperation("门店分类及分类下菜品(点餐页左侧+右侧)")
+    @ApiOperation("门店分类树(menuType=1 美食+cuisines;menuType=2 通用+prices;默认 1)")
     @GetMapping("/categories-with-cuisines")
-    public R<List<CategoryWithCuisinesVO>> getCategoriesWithCuisines(
+    public R<List<CategoryMenuGroupVO>> getCategoriesWithMenuGroups(
             @RequestParam Integer storeId,
-            @RequestParam(required = false) String keyword) {
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Integer menuType) {
         try {
-            return diningServiceFeign.getCategoriesWithCuisinesByStoreId(storeId, keyword);
+            return diningServiceFeign.getCategoriesWithMenuGroupsByStoreId(storeId, keyword, menuType);
         } catch (Exception e) {
             log.error("categories-with-cuisines: {}", e.getMessage(), e);
             return R.fail(e.getMessage());

+ 7 - 8
alien-store/src/main/java/shop/alien/store/feign/DiningServiceFeign.java

@@ -620,7 +620,9 @@ public interface DiningServiceFeign {
     R<StoreInfoWithHomepageCuisinesDTO> getStoreInfoWithHomepageCuisines(@PathVariable("storeId") Integer storeId);
 
     @GetMapping("/store/info/categories")
-    R<List<StoreCuisineCategory>> getStoreInfoCategories(@RequestParam("storeId") Integer storeId);
+    R<List<StoreCuisineCategory>> getStoreInfoCategories(
+            @RequestParam("storeId") Integer storeId,
+            @RequestParam(value = "menuType", required = false) Integer menuType);
 
     @GetMapping("/store/info/cuisines")
     R<List<StoreCuisine>> getStoreInfoCuisinesByCategoryId(@RequestParam("categoryId") Integer categoryId);
@@ -636,16 +638,13 @@ public interface DiningServiceFeign {
             @RequestParam("storeId") Integer storeId);
 
     /**
-     * 根据门店ID查询菜品种类及各类别下菜品(可选按菜品名称模糊查询)
-     *
-     * @param storeId 门店ID
-     * @param keyword 菜品名称模糊查询关键词(可选)
-     * @return R.data 为 List&lt;CategoryWithCuisinesVO&gt;,cuisines 每项含 originalPrice、currentPrice、hasActiveDiscount
+     * 分类树:menuType=1(默认)美食+cuisines;menuType=2 通用价目+prices
      */
     @GetMapping("/store/info/categories-with-cuisines")
-    R<List<CategoryWithCuisinesVO>> getCategoriesWithCuisinesByStoreId(
+    R<List<CategoryMenuGroupVO>> getCategoriesWithMenuGroupsByStoreId(
             @RequestParam("storeId") Integer storeId,
-            @RequestParam(value = "keyword", required = false) String keyword);
+            @RequestParam(value = "keyword", required = false) String keyword,
+            @RequestParam(value = "menuType", required = false) Integer menuType);
 
     /**
      * 删除菜品种类(仅解除绑定+逻辑删分类,价目表菜品不变)

+ 20 - 10
alien-store/src/main/java/shop/alien/store/service/StoreCuisineCategoryService.java

@@ -2,6 +2,7 @@ package shop.alien.store.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
 import shop.alien.entity.store.StoreCuisineCategory;
+import shop.alien.entity.store.constants.StoreMenuType;
 
 import java.util.List;
 
@@ -14,21 +15,30 @@ import java.util.List;
 public interface StoreCuisineCategoryService extends IService<StoreCuisineCategory> {
 
     /**
-     * 查询菜品分类列表(按排序字段排序)
-     *
-     * @param storeId 门店ID
-     * @return List<StoreCuisineCategory>
+     * 查询分类列表(默认仅美食分类,与历史行为一致)
+     */
+    default List<StoreCuisineCategory> getCategoryList(Integer storeId) {
+        return getCategoryList(storeId, StoreMenuType.CUISINE);
+    }
+
+    /**
+     * 按门店与菜单维度查询分类({@link StoreMenuType})
      */
-    List<StoreCuisineCategory> getCategoryList(Integer storeId);
+    List<StoreCuisineCategory> getCategoryList(Integer storeId, Integer menuType);
 
     /**
-     * 批量创建菜品分类
+     * 批量创建分类(默认美食)
+     */
+    default boolean batchCreateCategories(Integer storeId, List<String> categoryNames) {
+        return batchCreateCategories(storeId, categoryNames, StoreMenuType.CUISINE);
+    }
+
+    /**
+     * 批量创建分类
      *
-     * @param storeId        门店ID
-     * @param categoryNames  分类名称列表
-     * @return boolean
+     * @param menuType {@link StoreMenuType#CUISINE} 或 {@link StoreMenuType#GENERIC_PRICE}
      */
-    boolean batchCreateCategories(Integer storeId, List<String> categoryNames);
+    boolean batchCreateCategories(Integer storeId, List<String> categoryNames, Integer menuType);
 
     /**
      * 更新菜品分类

+ 31 - 13
alien-store/src/main/java/shop/alien/store/service/impl/StoreCuisineCategoryServiceImpl.java

@@ -9,6 +9,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import shop.alien.entity.store.StoreCuisineCategory;
+import shop.alien.entity.store.constants.StoreMenuType;
 import shop.alien.mapper.StoreCuisineCategoryMapper;
 import shop.alien.store.service.StoreCuisineCategoryService;
 import shop.alien.util.common.JwtUtil;
@@ -30,21 +31,25 @@ import java.util.stream.Collectors;
 public class StoreCuisineCategoryServiceImpl extends ServiceImpl<StoreCuisineCategoryMapper, StoreCuisineCategory> implements StoreCuisineCategoryService {
 
     @Override
-    public List<StoreCuisineCategory> getCategoryList(Integer storeId) {
-        log.info("StoreCuisineCategoryServiceImpl.getCategoryList?storeId={}", storeId);
-        
+    public List<StoreCuisineCategory> getCategoryList(Integer storeId, Integer menuType) {
+        int mt = StoreMenuType.normalizeOrCuisine(menuType);
+        log.info("StoreCuisineCategoryServiceImpl.getCategoryList?storeId={}&menuType={}", storeId, mt);
+
         LambdaQueryWrapper<StoreCuisineCategory> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(StoreCuisineCategory::getStoreId, storeId)
-                .eq(StoreCuisineCategory::getStatus, 1) // 只查询启用的分类
-                .orderByAsc(StoreCuisineCategory::getSort) // 按排序字段升序
-                .orderByDesc(StoreCuisineCategory::getCreatedTime); // 如果排序字段相同,按创建时间倒序
-        
+                .eq(StoreCuisineCategory::getMenuType, mt)
+                .eq(StoreCuisineCategory::getStatus, 1)
+                .orderByAsc(StoreCuisineCategory::getSort)
+                .orderByDesc(StoreCuisineCategory::getCreatedTime);
+
         return this.list(wrapper);
     }
 
     @Override
-    public boolean batchCreateCategories(Integer storeId, List<String> categoryNames) {
-        log.info("StoreCuisineCategoryServiceImpl.batchCreateCategories?storeId={}&categoryNames={}", storeId, categoryNames);
+    public boolean batchCreateCategories(Integer storeId, List<String> categoryNames, Integer menuType) {
+        int mt = StoreMenuType.normalizeOrCuisine(menuType);
+        log.info("StoreCuisineCategoryServiceImpl.batchCreateCategories?storeId={}&categoryNames={}&menuType={}",
+                storeId, categoryNames, mt);
         
         // 从JWT获取当前登录用户ID
         Integer userId = getCurrentUserId();
@@ -62,9 +67,10 @@ public class StoreCuisineCategoryServiceImpl extends ServiceImpl<StoreCuisineCat
             }
         }
         
-        // 检查分类名称是否已存在
+        // 检查分类名称是否已存在(同店同 menu_type 下名称唯一)
         LambdaQueryWrapper<StoreCuisineCategory> checkWrapper = new LambdaQueryWrapper<>();
         checkWrapper.eq(StoreCuisineCategory::getStoreId, storeId)
+                .eq(StoreCuisineCategory::getMenuType, mt)
                 .in(StoreCuisineCategory::getCategoryName, categoryNames);
         List<StoreCuisineCategory> existingCategories = this.list(checkWrapper);
         
@@ -76,9 +82,10 @@ public class StoreCuisineCategoryServiceImpl extends ServiceImpl<StoreCuisineCat
             throw new RuntimeException("此分类名称已存在:" + String.join(",", existingNames));
         }
 
-        // 查询当前最大的排序值
+        // 查询当前 menu_type 下最大的排序值
         LambdaQueryWrapper<StoreCuisineCategory> maxSortWrapper = new LambdaQueryWrapper<>();
         maxSortWrapper.eq(StoreCuisineCategory::getStoreId, storeId)
+                .eq(StoreCuisineCategory::getMenuType, mt)
                 .orderByDesc(StoreCuisineCategory::getSort)
                 .last("LIMIT 1");
         StoreCuisineCategory maxSortCategory = this.getOne(maxSortWrapper);
@@ -92,6 +99,7 @@ public class StoreCuisineCategoryServiceImpl extends ServiceImpl<StoreCuisineCat
                 .map(categoryName -> {
                     StoreCuisineCategory category = new StoreCuisineCategory();
                     category.setStoreId(storeId);
+                    category.setMenuType(mt);
                     category.setCategoryName(categoryName);
                     category.setStatus(1); // 默认启用
                     category.setSort(sortCounter.incrementAndGet()); // 设置排序值
@@ -123,6 +131,8 @@ public class StoreCuisineCategoryServiceImpl extends ServiceImpl<StoreCuisineCat
         if (!categoryName.equals(category.getCategoryName())) {
             LambdaQueryWrapper<StoreCuisineCategory> wrapper = new LambdaQueryWrapper<>();
             wrapper.eq(StoreCuisineCategory::getStoreId, category.getStoreId())
+                    .eq(StoreCuisineCategory::getMenuType, category.getMenuType() != null
+                            ? category.getMenuType() : StoreMenuType.CUISINE)
                     .eq(StoreCuisineCategory::getCategoryName, categoryName)
                     .ne(StoreCuisineCategory::getId, id);
             StoreCuisineCategory existingCategory = this.getOne(wrapper);
@@ -172,11 +182,19 @@ public class StoreCuisineCategoryServiceImpl extends ServiceImpl<StoreCuisineCat
         LambdaQueryWrapper<StoreCuisineCategory> checkWrapper = new LambdaQueryWrapper<>();
         checkWrapper.eq(StoreCuisineCategory::getStoreId, storeId)
                 .in(StoreCuisineCategory::getId, categoryIds);
-        long count = this.count(checkWrapper);
-        if (count != categoryIds.size()) {
+        List<StoreCuisineCategory> owned = this.list(checkWrapper);
+        if (owned.size() != categoryIds.size()) {
             log.warn("更新菜品分类排序失败:部分分类ID不属于该门店");
             throw new RuntimeException("部分分类ID不属于该门店");
         }
+        long distinctTypes = owned.stream()
+                .map(c -> c.getMenuType() != null ? c.getMenuType() : StoreMenuType.CUISINE)
+                .distinct()
+                .count();
+        if (distinctTypes > 1) {
+            log.warn("更新菜品分类排序失败:一次仅允许同一 menu_type 的分类参与排序");
+            throw new RuntimeException("排序列表需全部为美食分类或全部为通用价目分类");
+        }
 
         // 从JWT获取当前登录用户ID
         Integer userId = getCurrentUserId();