Преглед изворни кода

实现通用点餐接口 feign同步接口信息 二维码存储商铺类型 1美食2通用

lutong пре 2 недеља
родитељ
комит
345ba2b982
18 измењених фајлова са 717 додато и 61 уклоњено
  1. 24 2
      alien-dining/docs/通用价目点餐接口说明.md
  2. 3 3
      alien-dining/src/main/java/shop/alien/dining/controller/DiningController.java
  3. 2 2
      alien-dining/src/main/java/shop/alien/dining/controller/GenericDiningController.java
  4. 23 4
      alien-dining/src/main/java/shop/alien/dining/service/impl/CartServiceImpl.java
  5. 44 7
      alien-dining/src/main/java/shop/alien/dining/service/impl/DiningServiceImpl.java
  6. 13 13
      alien-dining/src/main/java/shop/alien/dining/service/impl/GenericCartServiceImpl.java
  7. 7 6
      alien-dining/src/main/java/shop/alien/dining/service/impl/GenericDiningServiceImpl.java
  8. 6 4
      alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java
  9. 43 0
      alien-dining/src/main/java/shop/alien/dining/support/StoreBusinessSectionOrdering.java
  10. 198 0
      alien-store/src/main/java/shop/alien/store/controller/dining/StoreGenericDiningPathProxyController.java
  11. 150 0
      alien-store/src/main/java/shop/alien/store/controller/dining/StoreGenericOrderPathProxyController.java
  12. 121 0
      alien-store/src/main/java/shop/alien/store/feign/DiningServiceFeign.java
  13. 10 2
      alien-store/src/main/java/shop/alien/store/service/TableAppQrCodeService.java
  14. 13 1
      alien-store/src/main/java/shop/alien/store/service/WeChatMiniProgramQrCodeService.java
  15. 12 1
      alien-store/src/main/java/shop/alien/store/service/dining/DiningSseProxyService.java
  16. 37 4
      alien-store/src/main/java/shop/alien/store/service/impl/StoreTableServiceImpl.java
  17. 4 2
      alien-store/src/main/java/shop/alien/store/service/impl/TableAppQrCodeServiceImpl.java
  18. 7 10
      alien-store/src/main/java/shop/alien/store/service/impl/WeChatMiniProgramQrCodeServiceImpl.java

+ 24 - 2
alien-dining/docs/通用价目点餐接口说明.md

@@ -35,6 +35,13 @@
 
 网关若有统一前缀(如 `/api`),请在下列路径前自行拼接。
 
+**alien-store 透传:** 与美食点餐相同,在 store 侧增加 Feign + 代理控制器后,APP/统一网关可使用 **与 dining 完全一致** 的路径调用:
+
+- `GET/POST .../store/generic-dining/...` → `DiningServiceFeign` 透传 alien-dining  
+- `GET/POST .../store/generic-order/...` → 同上;**SSE**:`GET .../store/generic-order/sse/{tableId}`(`DiningSseProxyService.proxyGenericOrderSse`)
+
+支付、订单详情等仍用 **`/store/order/...`**(与美食共用)。
+
 ### 3.1 点餐页 / 券 / 锁单 / 结算预览
 
 **前缀:** `/store/generic-dining`
@@ -135,7 +142,22 @@
 
 ---
 
-## 6. 兼容性说明(旧代码是否仍可用)
+## 6. 商家后台:创建餐桌、价目进分类(要不要改接口?)
+
+### 6.1 价目表绑定分类
+
+- **一般不需要新接口。** `store_price.category_ids` 存 **JSON 数组字符串**(如 `[1,2,3]`),与 `store_booking_category` 等分类 id 对应即可。
+- 使用门店侧已有 **`POST /store/price/save`**、**`POST /store/price/update`**(`StorePriceController`),在 body 里带上 **`categoryIds`**(或实体映射字段)即可。
+- 小程序通用点餐列表按分类筛选依赖该字段为 **合法 JSON**;非法 JSON 可能导致 `JSON_CONTAINS` 查询异常。
+
+### 6.2 创建餐桌(`store_table.type`)
+
+- **`POST /store/booking/table/add`**(`StoreBookingTableController`,body:`StoreBookingTableBatchDTO`)**已支持**字段 **`type`**:**`1` = 美食**,**`2` = 通用**;不传默认 **`1`**。建通用价目桌时传 **`type: 2`** 即可,**无需再单独加接口**。
+- 若仍使用 **`StoreTableController` 的批量建桌**(如 **`batchCreateTables`** → `StoreTableServiceImpl.batchCreateTables`),当前实现 **不会写入 `type`**,新桌为 **`NULL`**,下单侧会按 **美食桌** 处理。若必须通过该入口建 **通用桌**,需要 **扩展 DTO + 服务**,在落库时设置 **`type = 2`**(或统一改为走预订桌 **`/store/booking/table/add`**)。
+
+---
+
+## 7. 兼容性说明(旧代码是否仍可用)
 
 - **旧 URL 未被移除**:继续使用 `/store/dining`、`/store/order` 的美食流程在设计上仍支持。
 - **前提:** 已执行 DDL;美食桌 **`store_table.type` 不为 `2`**(`NULL` 一般仍走美食)。
@@ -143,7 +165,7 @@
 
 ---
 
-## 7. 关键代码位置(便于维护)
+## 8. 关键代码位置(便于维护)
 
 | 内容 | 路径 |
 |------|------|

+ 3 - 3
alien-dining/src/main/java/shop/alien/dining/controller/DiningController.java

@@ -36,7 +36,7 @@ public class DiningController {
     private final DiningWalkInReservationService diningWalkInReservationService;
     private final AlienStoreFeign alienStoreFeign;
 
-    @ApiOperation(value = "提交到店就餐信息", notes = "选完人数并填写姓名/电话/时段后调用:写入 user_reservation,状态为已到店(2),并关联当前桌;同时将 store_table 置为就餐中(1)、写入 diner_count(与 page-info 首客传人数一致),便于 /table-dining-status。返回预约ID。下单时可不传 userReservationId,后端将按 tableId 解析本桌当日有效预约。startTime、endTime(若传)必须为 yyyy-MM-dd HH:mm(两位时分、无秒);不传 endTime 由后端推算。可选 reservationDate 须与 startTime 日期一致。")
+    @ApiOperation(value = "提交到店就餐信息", notes = "选完人数并填写姓名/电话/时段后调用:写入 user_reservation,状态为已到店(2),并关联当前桌;此时将 store_table 置为就餐中(1)、并写入 diner_count(此前 page-info 仅保存人数不会开台)。便于 /table-dining-status。返回预约ID。下单时可不传 userReservationId,后端将按 tableId 解析本桌当日有效预约。startTime、endTime(若传)必须为 yyyy-MM-dd HH:mm(两位时分、无秒);不传 endTime 由后端推算。可选 reservationDate 须与 startTime 日期一致。")
     @PostMapping("/walk-in/reservation")
     public R<Integer> createWalkInReservation(@Valid @RequestBody DiningWalkInReservationDTO dto) {
         log.info("DiningController.createWalkInReservation?tableId={}", dto != null ? dto.getTableId() : null);
@@ -108,11 +108,11 @@ public class DiningController {
         }
     }
 
-    @ApiOperation(value = "获取点餐页面信息", notes = "首客选桌时传就餐人数,餐桌置为就餐中并保存人数;后续用户选同一桌可不传就餐人数,将使用表中已保存人数")
+    @ApiOperation(value = "获取点餐页面信息", notes = "首客传就餐人数时仅保存人数不把餐桌置为就餐中;提交到店/用餐信息(walk-in)后才会开台。后续同一桌可不传人数,用表中已保存人数")
     @GetMapping("/page-info")
     public R<DiningPageInfoVO> getDiningPageInfo(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
-            @ApiParam(value = "就餐人数(首客必传;餐桌已就餐中时可省略,将使用已保存人数)") @RequestParam(required = false) Integer dinerCount) {
+            @ApiParam(value = "就餐人数(首客必传;若表中已有已保存人数可省略)") @RequestParam(required = false) Integer dinerCount) {
         log.info("DiningController.getDiningPageInfo?tableId={}, dinerCount={}", tableId, dinerCount);
         try {
             // 从 token 获取用户信息

+ 2 - 2
alien-dining/src/main/java/shop/alien/dining/controller/GenericDiningController.java

@@ -29,11 +29,11 @@ public class GenericDiningController {
     private final GenericDiningService genericDiningService;
     private final DiningService diningService;
 
-    @ApiOperation(value = "获取点餐页面信息", notes = "逻辑同美食点餐 page-info,仅通用价目桌(type=2)可用")
+    @ApiOperation(value = "获取点餐页面信息", notes = "同美食 page-info:人数只落库,开台在 walk-in;仅 type=2 桌可用")
     @GetMapping("/page-info")
     public R<DiningPageInfoVO> getDiningPageInfo(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
-            @ApiParam(value = "就餐人数(首客必传;餐桌已就餐中时可省略)") @RequestParam(required = false) Integer dinerCount) {
+            @ApiParam(value = "就餐人数(首客必传;若表中已有已保存人数可省略)") @RequestParam(required = false) Integer dinerCount) {
         log.info("GenericDiningController.getDiningPageInfo?tableId={}, dinerCount={}", tableId, dinerCount);
         try {
             Integer userId = TokenUtil.getCurrentUserId();

+ 23 - 4
alien-dining/src/main/java/shop/alien/dining/service/impl/CartServiceImpl.java

@@ -24,6 +24,7 @@ import shop.alien.mapper.StoreInfoMapper;
 import shop.alien.mapper.StoreProductDiscountRuleMapper;
 import shop.alien.mapper.StoreTableMapper;
 import shop.alien.dining.support.DiningMenuPricing;
+import shop.alien.dining.support.StoreBusinessSectionOrdering;
 import shop.alien.dining.support.StoreCartMenuFilters;
 import shop.alien.dining.constants.OrderMenuConstants;
 import shop.alien.dining.util.TokenUtil;
@@ -71,6 +72,16 @@ public class CartServiceImpl implements CartService {
     private final StoreInfoMapper storeInfoMapper;
     private final StoreProductDiscountRuleMapper storeProductDiscountRuleMapper;
 
+    private void assertCuisineTableForCart(StoreTable table) {
+        if (table == null) {
+            throw new RuntimeException("桌号不存在");
+        }
+        StoreInfo info = storeInfoMapper.selectById(table.getStoreId());
+        if (!StoreBusinessSectionOrdering.allowsCuisinePricing(info, table)) {
+            throw new RuntimeException("当前桌台不适用美食购物车,请使用通用价目入口");
+        }
+    }
+
     @Override
     public CartDTO getCart(Integer tableId) {
         log.info("获取购物车, tableId={}", tableId);
@@ -250,15 +261,16 @@ public class CartServiceImpl implements CartService {
         log.info("添加商品到购物车, dto={}", dto);
         // 验证桌号
         StoreTable table = storeTableMapper.selectById(dto.getTableId());
-        if (table == null) {
-            throw new RuntimeException("桌号不存在");
-        }
+        assertCuisineTableForCart(table);
 
         // 验证菜品
         StoreCuisine cuisine = storeCuisineMapper.selectById(dto.getCuisineId());
         if (cuisine == null) {
             throw new RuntimeException("菜品不存在");
         }
+        if (!Objects.equals(cuisine.getStoreId(), table.getStoreId())) {
+            throw new RuntimeException("菜品与桌台门店不匹配");
+        }
         if (cuisine.getShelfStatus() != 1) {
             throw new RuntimeException("菜品已下架");
         }
@@ -353,12 +365,18 @@ public class CartServiceImpl implements CartService {
         } else {
             // 商品不存在,自动添加
             log.info("商品不在购物车中,自动添加, tableId={}, cuisineId={}, quantity={}", tableId, cuisineId, quantity);
-            
+
+            StoreTable table = storeTableMapper.selectById(tableId);
+            assertCuisineTableForCart(table);
+
             // 验证菜品
             StoreCuisine cuisine = storeCuisineMapper.selectById(cuisineId);
             if (cuisine == null) {
                 throw new RuntimeException("菜品不存在");
             }
+            if (!Objects.equals(cuisine.getStoreId(), table.getStoreId())) {
+                throw new RuntimeException("菜品与桌台门店不匹配");
+            }
             if (cuisine.getShelfStatus() != 1) {
                 throw new RuntimeException("菜品已下架");
             }
@@ -563,6 +581,7 @@ public class CartServiceImpl implements CartService {
         if (toTable == null) {
             throw new RuntimeException("目标桌号不存在");
         }
+        assertCuisineTableForCart(toTable);
 
         // 获取目标购物车
         CartDTO toCart = getCart(toTableId);

+ 44 - 7
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningServiceImpl.java

@@ -15,6 +15,7 @@ import shop.alien.dining.config.BaseRedisService;
 import shop.alien.dining.service.CartService;
 import shop.alien.dining.service.DiningService;
 import shop.alien.dining.support.DiningOrderServiceFeeCalculator;
+import shop.alien.dining.support.StoreBusinessSectionOrdering;
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
@@ -95,20 +96,20 @@ public class DiningServiceImpl implements DiningService {
         if (storeInfo == null) {
             throw new RuntimeException("门店不存在");
         }
+        if (!StoreBusinessSectionOrdering.allowsCuisinePricing(storeInfo, table)) {
+            throw new RuntimeException("当前门店适用通用价目点餐,请使用通用价目入口");
+        }
 
-        // 第一个用户:传入用餐人数时,将餐桌置为就餐中并保存就餐人数
+        // 选择人数:只落库就餐人数,不把餐桌置为就餐中;开台在提交到店/用餐信息(walk-in)时处理
         if (dinerCount != null && dinerCount > 0) {
-            table.setStatus(1); // 就餐中
             table.setDinerCount(dinerCount);
             storeTableMapper.updateById(table);
-            log.info("首客选桌并填写用餐人数, tableId={}, dinerCount={}, 餐桌状态已置为就餐中", tableId, dinerCount);
+            log.info("首客填写用餐人数(餐桌状态不变,未开台), tableId={}, dinerCount={}", tableId, dinerCount);
         } else {
-            // 第二个及后续用户:若餐桌已就餐中且表内已有就餐人数,直接使用
-            if (Integer.valueOf(1).equals(table.getStatus()) && table.getDinerCount() != null && table.getDinerCount() > 0) {
+            if (table.getDinerCount() != null && table.getDinerCount() > 0) {
                 dinerCount = table.getDinerCount();
-                log.info("餐桌已就餐中,使用已保存的就餐人数, tableId={}, dinerCount={}", tableId, dinerCount);
+                log.info("使用已保存的就餐人数, tableId={}, dinerCount={}, tableStatus={}", tableId, dinerCount, table.getStatus());
             } else {
-                // 餐桌空闲且未传就餐人数时要求必填
                 throw new RuntimeException("请选择用餐人数");
             }
         }
@@ -127,6 +128,18 @@ public class DiningServiceImpl implements DiningService {
     public List<CuisineListVO> searchCuisines(Integer storeId, String keyword, Integer tableId) {
         log.info("搜索菜品, storeId={}, keyword={}, tableId={}", storeId, keyword, tableId);
 
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table == null) {
+            throw new RuntimeException("桌号不存在");
+        }
+        if (!Objects.equals(table.getStoreId(), storeId)) {
+            throw new RuntimeException("门店与桌台不匹配");
+        }
+        StoreInfo storeInfoForTable = storeInfoMapper.selectById(storeId);
+        if (!StoreBusinessSectionOrdering.allowsCuisinePricing(storeInfoForTable, table)) {
+            throw new RuntimeException("当前门店适用通用价目点餐,请使用通用价目入口");
+        }
+
         // 限制搜索关键词长度
         if (StringUtils.hasText(keyword) && keyword.length() > 10) {
             keyword = keyword.substring(0, 10);
@@ -149,6 +162,18 @@ public class DiningServiceImpl implements DiningService {
     public List<CuisineListVO> getCuisinesByCategory(Integer storeId, Integer categoryId, Integer tableId, Integer page, Integer size) {
         log.info("根据分类获取菜品列表, storeId={}, categoryId={}, tableId={}, page={}, size={}", storeId, categoryId, tableId, page, size);
 
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table == null) {
+            throw new RuntimeException("桌号不存在");
+        }
+        if (!Objects.equals(table.getStoreId(), storeId)) {
+            throw new RuntimeException("门店与桌台不匹配");
+        }
+        StoreInfo storeInfoForTable = storeInfoMapper.selectById(storeId);
+        if (!StoreBusinessSectionOrdering.allowsCuisinePricing(storeInfoForTable, table)) {
+            throw new RuntimeException("当前门店适用通用价目点餐,请使用通用价目入口");
+        }
+
         if (page == null || page < 1) {
             page = 1;
         }
@@ -183,6 +208,18 @@ public class DiningServiceImpl implements DiningService {
             throw new RuntimeException("菜品不存在");
         }
 
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table == null) {
+            throw new RuntimeException("桌号不存在");
+        }
+        if (!Objects.equals(cuisine.getStoreId(), table.getStoreId())) {
+            throw new RuntimeException("菜品与桌台门店不匹配");
+        }
+        StoreInfo storeInfoForTable = storeInfoMapper.selectById(table.getStoreId());
+        if (!StoreBusinessSectionOrdering.allowsCuisinePricing(storeInfoForTable, table)) {
+            throw new RuntimeException("当前门店适用通用价目点餐,请使用通用价目入口");
+        }
+
         CuisineDetailVO vo = new CuisineDetailVO();
         // 手动映射所有字段,确保所有属性都被正确复制
         vo.setId(cuisine.getId());

+ 13 - 13
alien-dining/src/main/java/shop/alien/dining/service/impl/GenericCartServiceImpl.java

@@ -25,6 +25,7 @@ import shop.alien.mapper.StoreInfoMapper;
 import shop.alien.mapper.StoreProductDiscountRuleMapper;
 import shop.alien.mapper.StoreTableMapper;
 import shop.alien.dining.support.DiningMenuPricing;
+import shop.alien.dining.support.StoreBusinessSectionOrdering;
 import shop.alien.dining.support.StoreCartMenuFilters;
 import shop.alien.dining.util.TokenUtil;
 
@@ -68,6 +69,16 @@ public class GenericCartServiceImpl implements GenericCartService {
     private final StoreInfoMapper storeInfoMapper;
     private final StoreProductDiscountRuleMapper storeProductDiscountRuleMapper;
 
+    private void assertGenericTableForCart(StoreTable table) {
+        if (table == null) {
+            throw new RuntimeException("桌号不存在");
+        }
+        StoreInfo info = storeInfoMapper.selectById(table.getStoreId());
+        if (!StoreBusinessSectionOrdering.allowsGenericPricing(info, table)) {
+            throw new RuntimeException("当前桌台不适用通用价目购物车,请使用美食点餐入口");
+        }
+    }
+
     @Override
     public CartDTO getCart(Integer tableId) {
         log.info("获取通用价目购物车, tableId={}", tableId);
@@ -245,14 +256,8 @@ public class GenericCartServiceImpl implements GenericCartService {
     @Override
     public CartDTO addItem(AddCartItemDTO dto) {
         log.info("添加商品到购物车, dto={}", dto);
-        // 验证桌号(须为通用价目桌 type=2)
         StoreTable table = storeTableMapper.selectById(dto.getTableId());
-        if (table == null) {
-            throw new RuntimeException("桌号不存在");
-        }
-        if (table.getType() == null || !Integer.valueOf(2).equals(table.getType())) {
-            throw new RuntimeException("当前桌台不是通用价目桌,无法使用本购物车");
-        }
+        assertGenericTableForCart(table);
 
         // dto.cuisineId 此处表示 store_price.id
         StorePrice price = storePriceMapper.selectById(dto.getCuisineId());
@@ -356,12 +361,7 @@ public class GenericCartServiceImpl implements GenericCartService {
         } else {
             log.info("商品不在购物车中,自动添加, tableId={}, priceId={}, quantity={}", tableId, cuisineId, quantity);
             StoreTable t = storeTableMapper.selectById(tableId);
-            if (t == null) {
-                throw new RuntimeException("桌号不存在");
-            }
-            if (t.getType() == null || !Integer.valueOf(2).equals(t.getType())) {
-                throw new RuntimeException("当前桌台不是通用价目桌");
-            }
+            assertGenericTableForCart(t);
             StorePrice price = storePriceMapper.selectById(cuisineId);
             if (price == null) {
                 throw new RuntimeException("价目项不存在");

+ 7 - 6
alien-dining/src/main/java/shop/alien/dining/service/impl/GenericDiningServiceImpl.java

@@ -11,6 +11,7 @@ import shop.alien.dining.service.DiningService;
 import shop.alien.dining.service.GenericCartService;
 import shop.alien.dining.service.GenericDiningService;
 import shop.alien.dining.service.OrderLockService;
+import shop.alien.dining.support.StoreBusinessSectionOrdering;
 import shop.alien.entity.store.LifeDiscountCoupon;
 import shop.alien.entity.store.StoreInfo;
 import shop.alien.entity.store.StoreOrderDetail;
@@ -58,8 +59,9 @@ public class GenericDiningServiceImpl implements GenericDiningService {
         if (table == null) {
             throw new RuntimeException("桌号不存在");
         }
-        if (table.getType() == null || !Integer.valueOf(2).equals(table.getType())) {
-            throw new RuntimeException("当前桌台不是通用价目桌,请使用美食点餐入口");
+        StoreInfo storeInfo = storeInfoMapper.selectById(table.getStoreId());
+        if (!StoreBusinessSectionOrdering.allowsGenericPricing(storeInfo, table)) {
+            throw new RuntimeException("当前门店/桌台不适用通用价目点餐,请使用美食点餐入口");
         }
     }
 
@@ -75,14 +77,13 @@ public class GenericDiningServiceImpl implements GenericDiningService {
         }
 
         if (dinerCount != null && dinerCount > 0) {
-            table.setStatus(1);
             table.setDinerCount(dinerCount);
             storeTableMapper.updateById(table);
-            log.info("通用价目-首客选桌并填写用餐人数, tableId={}, dinerCount={}", tableId, dinerCount);
+            log.info("通用价目-首客填写用餐人数(餐桌状态不变,未开台), tableId={}, dinerCount={}", tableId, dinerCount);
         } else {
-            if (Integer.valueOf(1).equals(table.getStatus()) && table.getDinerCount() != null && table.getDinerCount() > 0) {
+            if (table.getDinerCount() != null && table.getDinerCount() > 0) {
                 dinerCount = table.getDinerCount();
-                log.info("通用价目-餐桌已就餐中,使用已保存人数, tableId={}, dinerCount={}", tableId, dinerCount);
+                log.info("通用价目-使用已保存人数, tableId={}, dinerCount={}, tableStatus={}", tableId, dinerCount, table.getStatus());
             } else {
                 throw new RuntimeException("请选择用餐人数");
             }

+ 6 - 4
alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java

@@ -14,6 +14,7 @@ import shop.alien.dining.config.BaseRedisService;
 import shop.alien.dining.service.CartService;
 import shop.alien.dining.service.StoreOrderService;
 import shop.alien.dining.support.DiningMenuPricing;
+import shop.alien.dining.support.StoreBusinessSectionOrdering;
 import shop.alien.dining.util.TokenUtil;
 import shop.alien.dining.constants.OrderMenuConstants;
 import shop.alien.dining.service.GenericCartService;
@@ -115,13 +116,14 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
             throw new RuntimeException("门店不存在");
         }
 
+        int effectiveMenuType = StoreBusinessSectionOrdering.resolveEffectiveMenuType(storeInfo, table);
         if (menuType == OrderMenuConstants.MENU_TYPE_GENERIC_PRICE) {
-            if (table.getType() == null || !Integer.valueOf(2).equals(table.getType())) {
-                throw new RuntimeException("当前桌台不是通用价目桌,请使用美食点餐入口");
+            if (effectiveMenuType != OrderMenuConstants.MENU_TYPE_GENERIC_PRICE) {
+                throw new RuntimeException("当前门店/桌台不适用通用价目点餐,请使用美食点餐入口");
             }
         } else {
-            if (table.getType() != null && Integer.valueOf(2).equals(table.getType())) {
-                throw new RuntimeException("当前桌台为通用价目桌,请使用通用点餐接口下单");
+            if (effectiveMenuType != OrderMenuConstants.MENU_TYPE_CUISINE) {
+                throw new RuntimeException("当前门店/桌台请使用通用价目点餐接口下单");
             }
         }
 

+ 43 - 0
alien-dining/src/main/java/shop/alien/dining/support/StoreBusinessSectionOrdering.java

@@ -0,0 +1,43 @@
+package shop.alien.dining.support;
+
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.StoreTable;
+
+/**
+ * 门店经营板块与桌台类型共同决定点餐维度(与桌码参数 m、alien-store 桌码生成逻辑一致):
+ * 优先 {@code store_info.business_section}——1 特色美食→美食(1);2 休闲娱乐、3 生活服务→通用价目(2);
+ * 板块未配置或非上述值时,再按 {@code store_table.type}(2→通用,否则→美食)。
+ */
+public final class StoreBusinessSectionOrdering {
+
+    private StoreBusinessSectionOrdering() {
+    }
+
+    /**
+     * @return 1 美食/store_cuisine;2 通用价目/store_price(见 {@link shop.alien.dining.constants.OrderMenuConstants})
+     */
+    public static int resolveEffectiveMenuType(StoreInfo storeInfo, StoreTable table) {
+        if (table == null) {
+            return 1;
+        }
+        if (storeInfo != null && storeInfo.getBusinessSection() != null) {
+            int sec = storeInfo.getBusinessSection();
+            if (sec == 1) {
+                return 1;
+            }
+            if (sec == 2 || sec == 3) {
+                return 2;
+            }
+        }
+        Integer t = table.getType();
+        return Integer.valueOf(2).equals(t) ? 2 : 1;
+    }
+
+    public static boolean allowsGenericPricing(StoreInfo storeInfo, StoreTable table) {
+        return resolveEffectiveMenuType(storeInfo, table) == 2;
+    }
+
+    public static boolean allowsCuisinePricing(StoreInfo storeInfo, StoreTable table) {
+        return resolveEffectiveMenuType(storeInfo, table) == 1;
+    }
+}

+ 198 - 0
alien-store/src/main/java/shop/alien/store/controller/dining/StoreGenericDiningPathProxyController.java

@@ -0,0 +1,198 @@
+package shop.alien.store.controller.dining;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.vo.*;
+import shop.alien.store.feign.DiningServiceFeign;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+
+/**
+ * 与 alien-dining 一致的路径前缀 {@code /store/generic-dining},透传通用价目点餐(store_price、type=2 桌)。
+ */
+@Slf4j
+@Api(tags = {"APP/统一网关-通用价目点餐(与小程序路径一致)"})
+@CrossOrigin
+@RestController
+@RequestMapping("/store/generic-dining")
+@RequiredArgsConstructor
+public class StoreGenericDiningPathProxyController {
+
+    private final DiningServiceFeign diningServiceFeign;
+
+    private String auth(HttpServletRequest request) {
+        String h = request.getHeader("Authorization");
+        if (h == null || h.isEmpty()) {
+            h = request.getHeader("authorization");
+        }
+        return h;
+    }
+
+    @ApiOperation(value = "获取点餐页面信息", notes = "透传 alien-dining /store/generic-dining/page-info;仅通用桌 type=2。")
+    @GetMapping("/page-info")
+    public R<DiningPageInfoVO> getDiningPageInfo(
+            HttpServletRequest request,
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "就餐人数") @RequestParam(required = false) Integer dinerCount) {
+        try {
+            return diningServiceFeign.getGenericDiningPageInfo(auth(request), tableId, dinerCount);
+        } catch (Exception e) {
+            log.error("generic getDiningPageInfo: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("搜索价目(store_price)")
+    @GetMapping("/search")
+    public R<List<CuisineListVO>> search(
+            HttpServletRequest request,
+            @RequestParam Integer storeId,
+            @RequestParam(required = false) String keyword,
+            @RequestParam Integer tableId) {
+        try {
+            return diningServiceFeign.genericSearchCuisines(auth(request), storeId, keyword, tableId);
+        } catch (Exception e) {
+            log.error("generic search: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("价目分页列表")
+    @GetMapping("/cuisines")
+    public R<List<CuisineListVO>> cuisines(
+            HttpServletRequest request,
+            @RequestParam Integer storeId,
+            @RequestParam(required = false) Integer categoryId,
+            @RequestParam Integer tableId,
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "12") Integer size) {
+        try {
+            return diningServiceFeign.getGenericCuisinesByCategory(auth(request), storeId, categoryId, tableId, page, size);
+        } catch (Exception e) {
+            log.error("generic cuisines: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("价目详情(priceItemId 为 store_price.id)")
+    @GetMapping("/cuisine/{priceItemId}")
+    public R<CuisineDetailVO> detail(
+            HttpServletRequest request,
+            @PathVariable Integer priceItemId,
+            @RequestParam Integer tableId) {
+        try {
+            return diningServiceFeign.getGenericPriceItemDetail(auth(request), priceItemId, tableId);
+        } catch (Exception e) {
+            log.error("generic cuisine detail: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("订单确认页")
+    @GetMapping("/order/confirm")
+    public R<OrderConfirmVO> orderConfirm(
+            HttpServletRequest request,
+            @RequestParam Integer tableId,
+            @RequestParam Integer dinerCount) {
+        try {
+            return diningServiceFeign.getGenericOrderConfirmInfo(auth(request), tableId, dinerCount);
+        } catch (Exception e) {
+            log.error("generic orderConfirm: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("可领取优惠券")
+    @GetMapping("/coupons/available")
+    public R<List<AvailableCouponVO>> availableCoupons(HttpServletRequest request, @RequestParam Integer storeId) {
+        try {
+            return diningServiceFeign.getGenericAvailableCoupons(auth(request), storeId);
+        } catch (Exception e) {
+            log.error("generic availableCoupons: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("领取优惠券")
+    @PostMapping("/coupon/receive")
+    public R<Boolean> receiveCoupon(HttpServletRequest request, @RequestParam Integer couponId) {
+        try {
+            return diningServiceFeign.receiveGenericCoupon(auth(request), couponId);
+        } catch (Exception e) {
+            log.error("generic receiveCoupon: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("锁定订单")
+    @PostMapping("/order/lock")
+    public R<Boolean> lockOrder(HttpServletRequest request, @RequestParam Integer tableId) {
+        try {
+            return diningServiceFeign.lockGenericOrder(auth(request), tableId);
+        } catch (Exception e) {
+            log.error("generic lockOrder: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("解锁订单")
+    @PostMapping("/order/unlock")
+    public R<Boolean> unlockOrder(HttpServletRequest request, @RequestParam Integer tableId) {
+        try {
+            return diningServiceFeign.unlockGenericOrder(auth(request), tableId);
+        } catch (Exception e) {
+            log.error("generic unlockOrder: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("查询订单锁")
+    @GetMapping("/order/check-lock")
+    public R<Integer> checkLock(HttpServletRequest request, @RequestParam Integer tableId) {
+        try {
+            return diningServiceFeign.checkGenericOrderLock(auth(request), tableId);
+        } catch (Exception e) {
+            log.error("generic checkLock: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("结算页信息")
+    @GetMapping("/order/settlement")
+    public R<OrderSettlementVO> settlement(HttpServletRequest request, @RequestParam Integer orderId) {
+        try {
+            return diningServiceFeign.getGenericOrderSettlementInfo(auth(request), orderId);
+        } catch (Exception e) {
+            log.error("generic settlement: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("锁定结算")
+    @PostMapping("/order/settlement/lock")
+    public R<Boolean> lockSettlement(HttpServletRequest request, @RequestParam Integer orderId) {
+        try {
+            return diningServiceFeign.lockGenericSettlement(auth(request), orderId);
+        } catch (Exception e) {
+            log.error("generic lockSettlement: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("解锁结算")
+    @PostMapping("/order/settlement/unlock")
+    public R<Boolean> unlockSettlement(HttpServletRequest request, @RequestParam Integer orderId) {
+        try {
+            return diningServiceFeign.unlockGenericSettlement(auth(request), orderId);
+        } catch (Exception e) {
+            log.error("generic unlockSettlement: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+}

+ 150 - 0
alien-store/src/main/java/shop/alien/store/controller/dining/StoreGenericOrderPathProxyController.java

@@ -0,0 +1,150 @@
+package shop.alien.store.controller.dining;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.AddCartItemDTO;
+import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.entity.store.dto.CreateOrderDTO;
+import shop.alien.entity.store.vo.OrderSuccessVO;
+import shop.alien.store.feign.DiningServiceFeign;
+import shop.alien.store.service.dining.DiningSseProxyService;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.validation.Valid;
+
+/**
+ * 与 alien-dining 一致的路径前缀 {@code /store/generic-order},透传通用价目购物车与创建订单。
+ * 支付/详情等仍走 {@link StoreOrderPathProxyController} 的 {@code /store/order}。
+ */
+@Slf4j
+@Api(tags = {"APP/统一网关-通用价目购物车与下单(与小程序路径一致)"})
+@CrossOrigin
+@RestController
+@RequestMapping("/store/generic-order")
+@RequiredArgsConstructor
+public class StoreGenericOrderPathProxyController {
+
+    private final DiningServiceFeign diningServiceFeign;
+    private final DiningSseProxyService diningSseProxyService;
+
+    private String auth(HttpServletRequest request) {
+        String h = request.getHeader("Authorization");
+        if (h == null || h.isEmpty()) {
+            h = request.getHeader("authorization");
+        }
+        return h;
+    }
+
+    @ApiOperation("通用价目购物车")
+    @GetMapping("/cart/{tableId}")
+    public R<CartDTO> getCart(HttpServletRequest request, @PathVariable Integer tableId) {
+        try {
+            return diningServiceFeign.getGenericCart(auth(request), tableId);
+        } catch (Exception e) {
+            log.error("generic getCart: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("加购(cuisineId 传 store_price.id)")
+    @PostMapping("/cart/add")
+    public R<CartDTO> addCartItem(HttpServletRequest request, @Valid @RequestBody AddCartItemDTO dto) {
+        try {
+            return diningServiceFeign.addGenericCartItem(auth(request), dto);
+        } catch (Exception e) {
+            log.error("generic addCartItem: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("更新购物车数量")
+    @PutMapping("/cart/update")
+    public R<CartDTO> updateCartItem(
+            HttpServletRequest request,
+            @RequestParam Integer tableId,
+            @RequestParam Integer cuisineId,
+            @RequestParam Integer quantity) {
+        try {
+            return diningServiceFeign.updateGenericCartItem(auth(request), tableId, cuisineId, quantity);
+        } catch (Exception e) {
+            log.error("generic updateCartItem: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("删除购物车行")
+    @DeleteMapping("/cart/remove")
+    public R<CartDTO> removeCartItem(
+            HttpServletRequest request,
+            @RequestParam Integer tableId,
+            @RequestParam Integer cuisineId) {
+        try {
+            return diningServiceFeign.removeGenericCartItem(auth(request), tableId, cuisineId);
+        } catch (Exception e) {
+            log.error("generic removeCartItem: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("清空购物车")
+    @DeleteMapping("/cart/clear")
+    public R<CartDTO> clearCart(HttpServletRequest request, @RequestParam Integer tableId) {
+        try {
+            return diningServiceFeign.clearGenericCart(auth(request), tableId);
+        } catch (Exception e) {
+            log.error("generic clearCart: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("设置就餐人数")
+    @PostMapping("/cart/set-diner-count")
+    public R<CartDTO> setDinerCount(
+            HttpServletRequest request,
+            @RequestParam Integer tableId,
+            @RequestParam Integer dinerCount) {
+        try {
+            return diningServiceFeign.setGenericDinerCount(auth(request), tableId, dinerCount);
+        } catch (Exception e) {
+            log.error("generic setDinerCount: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("更新餐具数量")
+    @PutMapping("/cart/update-tableware")
+    public R<CartDTO> updateTablewareQuantity(
+            HttpServletRequest request,
+            @RequestParam Integer tableId,
+            @RequestParam(required = false) Integer quantity) {
+        try {
+            return diningServiceFeign.updateGenericTablewareQuantity(auth(request), tableId, quantity);
+        } catch (Exception e) {
+            log.error("generic updateTablewareQuantity: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("创建订单(通用价目)")
+    @PostMapping("/create")
+    public R<OrderSuccessVO> createOrder(HttpServletRequest request, @Valid @RequestBody CreateOrderDTO dto) {
+        try {
+            return diningServiceFeign.createGenericOrder(auth(request), dto);
+        } catch (Exception e) {
+            log.error("generic createOrder: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "SSE", notes = "透传 alien-dining /store/generic-order/sse/{tableId}")
+    @GetMapping(value = "/sse/{tableId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+    public SseEmitter sse(HttpServletRequest request, @PathVariable Integer tableId) {
+        return diningSseProxyService.proxyGenericOrderSse(tableId, auth(request));
+    }
+}

+ 121 - 0
alien-store/src/main/java/shop/alien/store/feign/DiningServiceFeign.java

@@ -197,6 +197,127 @@ public interface DiningServiceFeign {
             @RequestHeader(value = "Authorization", required = false) String authorization,
             @RequestParam("orderId") Integer orderId);
 
+    // ==================== 通用价目点餐(/store/generic-dining、/store/generic-order) ====================
+
+    @GetMapping("/store/generic-dining/page-info")
+    R<DiningPageInfoVO> getGenericDiningPageInfo(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("tableId") Integer tableId,
+            @RequestParam(value = "dinerCount", required = false) Integer dinerCount);
+
+    @GetMapping("/store/generic-dining/search")
+    R<List<CuisineListVO>> genericSearchCuisines(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("storeId") Integer storeId,
+            @RequestParam(value = "keyword", required = false) String keyword,
+            @RequestParam("tableId") Integer tableId);
+
+    @GetMapping("/store/generic-dining/cuisines")
+    R<List<CuisineListVO>> getGenericCuisinesByCategory(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("storeId") Integer storeId,
+            @RequestParam(value = "categoryId", required = false) Integer categoryId,
+            @RequestParam("tableId") Integer tableId,
+            @RequestParam(value = "page", defaultValue = "1") Integer page,
+            @RequestParam(value = "size", defaultValue = "12") Integer size);
+
+    @GetMapping("/store/generic-dining/cuisine/{priceItemId}")
+    R<CuisineDetailVO> getGenericPriceItemDetail(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @PathVariable("priceItemId") Integer priceItemId,
+            @RequestParam("tableId") Integer tableId);
+
+    @GetMapping("/store/generic-dining/order/confirm")
+    R<OrderConfirmVO> getGenericOrderConfirmInfo(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("tableId") Integer tableId,
+            @RequestParam("dinerCount") Integer dinerCount);
+
+    @GetMapping("/store/generic-dining/coupons/available")
+    R<List<AvailableCouponVO>> getGenericAvailableCoupons(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("storeId") Integer storeId);
+
+    @PostMapping("/store/generic-dining/coupon/receive")
+    R<Boolean> receiveGenericCoupon(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("couponId") Integer couponId);
+
+    @PostMapping("/store/generic-dining/order/lock")
+    R<Boolean> lockGenericOrder(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("tableId") Integer tableId);
+
+    @PostMapping("/store/generic-dining/order/unlock")
+    R<Boolean> unlockGenericOrder(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("tableId") Integer tableId);
+
+    @GetMapping("/store/generic-dining/order/check-lock")
+    R<Integer> checkGenericOrderLock(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("tableId") Integer tableId);
+
+    @GetMapping("/store/generic-dining/order/settlement")
+    R<OrderSettlementVO> getGenericOrderSettlementInfo(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("orderId") Integer orderId);
+
+    @PostMapping("/store/generic-dining/order/settlement/lock")
+    R<Boolean> lockGenericSettlement(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("orderId") Integer orderId);
+
+    @PostMapping("/store/generic-dining/order/settlement/unlock")
+    R<Boolean> unlockGenericSettlement(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("orderId") Integer orderId);
+
+    @GetMapping("/store/generic-order/cart/{tableId}")
+    R<CartDTO> getGenericCart(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @PathVariable("tableId") Integer tableId);
+
+    @PostMapping("/store/generic-order/cart/add")
+    R<CartDTO> addGenericCartItem(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestBody AddCartItemDTO dto);
+
+    @PutMapping("/store/generic-order/cart/update")
+    R<CartDTO> updateGenericCartItem(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("tableId") Integer tableId,
+            @RequestParam("cuisineId") Integer cuisineId,
+            @RequestParam("quantity") Integer quantity);
+
+    @DeleteMapping("/store/generic-order/cart/remove")
+    R<CartDTO> removeGenericCartItem(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("tableId") Integer tableId,
+            @RequestParam("cuisineId") Integer cuisineId);
+
+    @DeleteMapping("/store/generic-order/cart/clear")
+    R<CartDTO> clearGenericCart(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("tableId") Integer tableId);
+
+    @PostMapping("/store/generic-order/cart/set-diner-count")
+    R<CartDTO> setGenericDinerCount(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("tableId") Integer tableId,
+            @RequestParam("dinerCount") Integer dinerCount);
+
+    @PutMapping("/store/generic-order/cart/update-tableware")
+    R<CartDTO> updateGenericTablewareQuantity(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("tableId") Integer tableId,
+            @RequestParam(value = "quantity", required = false) Integer quantity);
+
+    @PostMapping("/store/generic-order/create")
+    R<OrderSuccessVO> createGenericOrder(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestBody CreateOrderDTO dto);
+
     // ==================== 订单相关接口 ====================
 
     /**

+ 10 - 2
alien-store/src/main/java/shop/alien/store/service/TableAppQrCodeService.java

@@ -6,11 +6,19 @@ package shop.alien.store.service;
 public interface TableAppQrCodeService {
 
     /**
-     * 生成二维码图片并上传 OSS,内容为 {@code {"storeId":..,"tableNumber":".."}}
+     * 生成 APP 桌码(默认美食 {@code menuType=1})
+     */
+    default String generateTableAppQrCodeAndUpload(Integer storeId, String tableNumber) {
+        return generateTableAppQrCodeAndUpload(storeId, tableNumber, 1);
+    }
+
+    /**
+     * 生成二维码图片并上传 OSS,内容为 {@code {"storeId":..,"tableNumber":"..","menuType":1|2}}
      *
      * @param storeId      商户/门店 ID
      * @param tableNumber  桌号
+     * @param menuType     1=美食 2=通用价目,与 store_table.type 一致;非法或 null 按 1
      * @return OSS 访问 URL
      */
-    String generateTableAppQrCodeAndUpload(Integer storeId, String tableNumber);
+    String generateTableAppQrCodeAndUpload(Integer storeId, String tableNumber, Integer menuType);
 }

+ 13 - 1
alien-store/src/main/java/shop/alien/store/service/WeChatMiniProgramQrCodeService.java

@@ -37,11 +37,23 @@ public interface WeChatMiniProgramQrCodeService {
     String generateQrCodeAndUpload(String scene, String page, Integer width, String ossPath);
 
     /**
+     * 为桌号生成小程序二维码(默认按美食桌 {@code menuType=1})
+     *
+     * @param tableId   桌号ID
+     * @param storeId   门店ID
+     * @return 二维码URL
+     */
+    default String generateTableQrCode(Integer tableId, Integer storeId) {
+        return generateTableQrCode(tableId, storeId, 1);
+    }
+
+    /**
      * 为桌号生成小程序二维码
      *
      * @param tableId   桌号ID
      * @param storeId   门店ID
+     * @param menuType  桌台点餐类型:1=美食(store_cuisine)2=通用价目(store_price),与 store_table.type 一致;非法或 null 按 1
      * @return 二维码URL
      */
-    String generateTableQrCode(Integer tableId, Integer storeId);
+    String generateTableQrCode(Integer tableId, Integer storeId, Integer menuType);
 }

+ 12 - 1
alien-store/src/main/java/shop/alien/store/service/dining/DiningSseProxyService.java

@@ -30,6 +30,17 @@ public class DiningSseProxyService {
     private String diningBaseUrl;
 
     public SseEmitter proxyOrderSse(Integer tableId, String authorization) {
+        return proxySse(tableId, authorization, "/store/order/sse/");
+    }
+
+    /**
+     * 通用价目购物车 SSE,对应 alien-dining {@code /store/generic-order/sse/{tableId}}。
+     */
+    public SseEmitter proxyGenericOrderSse(Integer tableId, String authorization) {
+        return proxySse(tableId, authorization, "/store/generic-order/sse/");
+    }
+
+    private SseEmitter proxySse(Integer tableId, String authorization, String pathPrefix) {
         SseEmitter emitter = new SseEmitter(0L);
         String base = diningBaseUrl == null ? "" : diningBaseUrl.trim();
         if (base.isEmpty()) {
@@ -39,7 +50,7 @@ public class DiningSseProxyService {
         if (base.endsWith("/")) {
             base = base.substring(0, base.length() - 1);
         }
-        final String urlStr = base + "/store/order/sse/" + tableId;
+        final String urlStr = base + pathPrefix + tableId;
         EXECUTOR.execute(() -> {
             HttpURLConnection conn = null;
             try {

+ 37 - 4
alien-store/src/main/java/shop/alien/store/service/impl/StoreTableServiceImpl.java

@@ -12,11 +12,13 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.StoreInfo;
 import shop.alien.entity.store.StoreTable;
 import shop.alien.entity.store.StoreTableLog;
 import shop.alien.entity.store.dto.StoreTableChangeDTO;
 import shop.alien.entity.store.dto.StoreTableDTO;
 import shop.alien.entity.store.vo.StoreTableStatusVO;
+import shop.alien.mapper.StoreInfoMapper;
 import shop.alien.mapper.StoreTableLogMapper;
 import shop.alien.mapper.StoreTableMapper;
 import shop.alien.store.config.BaseRedisService;
@@ -42,6 +44,7 @@ import java.util.stream.Collectors;
 public class StoreTableServiceImpl extends ServiceImpl<StoreTableMapper, StoreTable> implements StoreTableService {
 
     private final StoreTableLogMapper storeTableLogMapper;
+    private final StoreInfoMapper storeInfoMapper;
     private final WeChatMiniProgramQrCodeService weChatMiniProgramQrCodeService;
     private final TableAppQrCodeService tableAppQrCodeService;
     private final BaseRedisService baseRedisService;
@@ -132,7 +135,8 @@ public class StoreTableServiceImpl extends ServiceImpl<StoreTableMapper, StoreTa
                     boolean any = false;
 
                     if (StringUtils.isBlank(table.getQrcodeUrl())) {
-                        String qrCodeUrl = weChatMiniProgramQrCodeService.generateTableQrCode(table.getId(), storeId);
+                        String qrCodeUrl = weChatMiniProgramQrCodeService.generateTableQrCode(table.getId(), storeId,
+                                resolveQrMenuTypeForTable(table));
                         if (StringUtils.isNotBlank(qrCodeUrl)) {
                             updateWrapper.set(StoreTable::getQrcodeUrl, qrCodeUrl);
                             any = true;
@@ -141,7 +145,8 @@ public class StoreTableServiceImpl extends ServiceImpl<StoreTableMapper, StoreTa
                     }
 
                     if (StringUtils.isBlank(table.getAppQrcodeUrl())) {
-                        String appUrl = tableAppQrCodeService.generateTableAppQrCodeAndUpload(storeId, table.getTableNumber());
+                        String appUrl = tableAppQrCodeService.generateTableAppQrCodeAndUpload(storeId, table.getTableNumber(),
+                                resolveQrMenuTypeForTable(table));
                         if (StringUtils.isNotBlank(appUrl)) {
                             updateWrapper.set(StoreTable::getAppQrcodeUrl, appUrl);
                             any = true;
@@ -218,8 +223,10 @@ public class StoreTableServiceImpl extends ServiceImpl<StoreTableMapper, StoreTa
                 return;
             }
 
-            String qrCodeUrl = weChatMiniProgramQrCodeService.generateTableQrCode(tableId, storeId);
-            String appUrl = tableAppQrCodeService.generateTableAppQrCodeAndUpload(storeId, table.getTableNumber());
+            String qrCodeUrl = weChatMiniProgramQrCodeService.generateTableQrCode(tableId, storeId,
+                    resolveQrMenuTypeForTable(table));
+            String appUrl = tableAppQrCodeService.generateTableAppQrCodeAndUpload(storeId, table.getTableNumber(),
+                    resolveQrMenuTypeForTable(table));
 
             LambdaUpdateWrapper<StoreTable> updateWrapper = new LambdaUpdateWrapper<>();
             updateWrapper.eq(StoreTable::getId, tableId);
@@ -393,6 +400,32 @@ public class StoreTableServiceImpl extends ServiceImpl<StoreTableMapper, StoreTa
     }
 
     /**
+     * 桌码中的点餐类型 m(1=美食/store_cuisine,2=通用价目/store_price):
+     * 优先按门店 {@code store_info.business_section}——1 特色美食→1;2 休闲娱乐、3 生活服务→2;
+     * 未配置时再按 {@code store_table.type}(2→2,否则→1)。
+     */
+    private int resolveQrMenuTypeForTable(StoreTable table) {
+        if (table == null) {
+            return 1;
+        }
+        Integer storeId = table.getStoreId();
+        if (storeId != null) {
+            StoreInfo info = storeInfoMapper.selectById(storeId);
+            if (info != null && info.getBusinessSection() != null) {
+                int sec = info.getBusinessSection();
+                if (sec == 1) {
+                    return 1;
+                }
+                if (sec == 2 || sec == 3) {
+                    return 2;
+                }
+            }
+        }
+        Integer t = table.getType();
+        return Integer.valueOf(2).equals(t) ? 2 : 1;
+    }
+
+    /**
      * 从JWT获取当前登录用户ID
      *
      * @return 用户ID,如果未登录返回null

+ 4 - 2
alien-store/src/main/java/shop/alien/store/service/impl/TableAppQrCodeServiceImpl.java

@@ -28,14 +28,16 @@ public class TableAppQrCodeServiceImpl implements TableAppQrCodeService {
     private final AliOSSUtil aliOSSUtil;
 
     @Override
-    public String generateTableAppQrCodeAndUpload(Integer storeId, String tableNumber) {
+    public String generateTableAppQrCodeAndUpload(Integer storeId, String tableNumber, Integer menuType) {
         if (storeId == null || StringUtils.isBlank(tableNumber)) {
             throw new IllegalArgumentException("storeId与tableNumber不能为空");
         }
+        int m = (menuType != null && menuType == 2) ? 2 : 1;
 
         JSONObject payload = new JSONObject();
         payload.put("storeId", storeId);
         payload.put("tableNumber", tableNumber.trim());
+        payload.put("menuType", m);
         String content = payload.toJSONString();
 
         byte[] png = encodeQrPng(content, QR_SIZE);
@@ -52,7 +54,7 @@ public class TableAppQrCodeServiceImpl implements TableAppQrCodeService {
             log.error("上传APP桌码到OSS失败, ossPath={}", ossPath);
             throw new RuntimeException("上传APP桌码到OSS失败");
         }
-        log.info("APP桌码生成并上传成功, storeId={}, tableNumber={}, url={}", storeId, tableNumber, url);
+        log.info("APP桌码生成并上传成功, storeId={}, tableNumber={}, menuType={}, url={}", storeId, tableNumber, m, url);
         return url;
     }
 

+ 7 - 10
alien-store/src/main/java/shop/alien/store/service/impl/WeChatMiniProgramQrCodeServiceImpl.java

@@ -275,26 +275,23 @@ public class WeChatMiniProgramQrCodeServiceImpl implements WeChatMiniProgramQrCo
     }
 
     @Override
-    public String generateTableQrCode(Integer tableId, Integer storeId) {
-        log.info("为桌号生成小程序二维码, tableId={}, storeId={}", tableId, storeId);
+    public String generateTableQrCode(Integer tableId, Integer storeId, Integer menuType) {
+        int m = (menuType != null && menuType == 2) ? 2 : 1;
+        log.info("为桌号生成小程序二维码, tableId={}, storeId={}, menuType={}", tableId, storeId, m);
 
         if (tableId == null || storeId == null) {
             throw new IllegalArgumentException("tableId和storeId不能为空");
         }
 
-        // 构建scene参数(格式:t=tableId&s=storeId,使用简写确保不超过32字符)
-        String scene = String.format("t=%d&s=%d", tableId, storeId);
-        
-        // 如果scene超过32字符,使用更简洁的格式
+        // scene≤32:微信 getwxacode/unlimit 限制;含点餐类型 m:1 美食 2 通用价目
+        String scene = String.format("t=%d&s=%d&m=%d", tableId, storeId, m);
         if (scene.length() > 32) {
-            scene = String.format("%d-%d", tableId, storeId);
+            scene = String.format("%d,%d,%d", tableId, storeId, m);
         }
 
-        // 构建OSS存储路径
-        String ossPath = String.format("qrcode/table/%d_%d_%d.png", 
+        String ossPath = String.format("qrcode/table/%d_%d_%d.png",
                 storeId, tableId, System.currentTimeMillis());
 
-        // 生成二维码并上传,扫码后打开指定页面
         return generateQrCodeAndUpload(scene, "pages/launch/index", 430, ossPath);
     }
 }