lutong 1 mese fa
parent
commit
dfc151b89b
16 ha cambiato i file con 1224 aggiunte e 38 eliminazioni
  1. 48 0
      alien-dining/src/main/java/shop/alien/dining/controller/DiningCollectController.java
  2. 80 8
      alien-dining/src/main/java/shop/alien/dining/controller/DiningCouponController.java
  3. 60 0
      alien-dining/src/main/java/shop/alien/dining/controller/DiningFileUploadController.java
  4. 80 2
      alien-dining/src/main/java/shop/alien/dining/feign/AlienStoreFeign.java
  5. 22 0
      alien-dining/src/main/java/shop/alien/dining/service/DiningCollectService.java
  6. 46 2
      alien-dining/src/main/java/shop/alien/dining/service/DiningCouponService.java
  7. 39 0
      alien-dining/src/main/java/shop/alien/dining/service/impl/DiningCollectServiceImpl.java
  8. 251 12
      alien-dining/src/main/java/shop/alien/dining/service/impl/DiningCouponServiceImpl.java
  9. 63 3
      alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java
  10. 1 1
      alien-entity/src/main/java/shop/alien/entity/store/StoreTable.java
  11. 5 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/OrderCuisineItemVO.java
  12. 12 6
      alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/LifeDiscountCouponPlatformServiceImpl.java
  13. 283 0
      alien-store/src/main/java/shop/alien/store/controller/DiningServiceController.java
  14. 218 0
      alien-store/src/main/java/shop/alien/store/feign/DiningServiceFeign.java
  15. 10 4
      alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponServiceImpl.java
  16. 6 0
      订单系统表结构说明.md

+ 48 - 0
alien-dining/src/main/java/shop/alien/dining/controller/DiningCollectController.java

@@ -0,0 +1,48 @@
+package shop.alien.dining.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiSort;
+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.LifeCollect;
+import shop.alien.dining.service.DiningCollectService;
+
+/**
+ * 点餐模块-收藏控制器(Feign 调 store,供小程序使用)
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/XX
+ */
+@Slf4j
+@Api(tags = {"微信点餐-收藏(用户端)"})
+@ApiSort(4)
+@CrossOrigin
+@RestController
+@RequestMapping("/dining/collect")
+@RequiredArgsConstructor
+public class DiningCollectController {
+
+    private final DiningCollectService diningCollectService;
+
+    /**
+     * 添加收藏
+     */
+    @ApiOperation(value = "添加收藏", notes = "添加收藏,支持收藏店铺、商品等")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/addCollect")
+    public R<Boolean> addCollect(@ApiParam(value = "收藏对象", required = true) @RequestBody LifeCollect lifeCollect) {
+        log.info("DiningCollectController.addCollect lifeCollect={}", lifeCollect);
+        try {
+            return diningCollectService.addCollect(lifeCollect);
+        } catch (Exception e) {
+            log.error("添加收藏失败: {}", e.getMessage(), e);
+            return R.fail("添加收藏失败: " + e.getMessage());
+        }
+    }
+}

+ 80 - 8
alien-dining/src/main/java/shop/alien/dining/controller/DiningCouponController.java

@@ -1,9 +1,11 @@
 package shop.alien.dining.controller;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiImplicitParam;
 import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
 import io.swagger.annotations.ApiParam;
 import io.swagger.annotations.ApiSort;
 import lombok.RequiredArgsConstructor;
@@ -39,20 +41,24 @@ public class DiningCouponController {
     /**
      * 获取当前用户优惠券列表(分 tab:全部/即将过期/已使用/已过期)
      */
-    @ApiOperation(value = "获取用户优惠券列表", notes = "需登录,请求头带 Authorization。tabType:0全部未使用,1即将过期,2已使用,3已过期")
+    @ApiOperation(value = "获取用户优惠券列表", notes = "需登录,请求头带 Authorization。tabType:0全部未使用,1即将过期,2已使用,3已过期。type:不传=优惠券+代金券,1=仅优惠券,4=仅代金券。couponType:1=仅满减券,2=仅折扣券,不传=全部优惠券")
     @GetMapping("/userList")
     @ApiImplicitParams({
             @ApiImplicitParam(name = "page", value = "页码", dataType = "int", paramType = "query", required = true),
             @ApiImplicitParam(name = "size", value = "每页条数", dataType = "int", paramType = "query", required = true),
-            @ApiImplicitParam(name = "tabType", value = "tab类型:0全部未使用,1即将过期,2已使用,3已过期", dataType = "String", paramType = "query", required = true)
+            @ApiImplicitParam(name = "tabType", value = "tab类型:0全部未使用,1即将过期,2已使用,3已过期", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "type", value = "券类型(不传:优惠券+代金券都返回,1:仅优惠券查 life_discount_coupon,4:仅代金券查 life_coupon)", dataType = "Integer", paramType = "query", required = false),
+            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选,仅当type不为4时有效)", dataType = "Integer", paramType = "query", required = false)
     })
     public R<List<LifeDiscountCouponVo>> getUserCouponList(
             HttpServletRequest request,
             @RequestParam(value = "page", defaultValue = "1") int page,
             @RequestParam(value = "size", defaultValue = "10") int size,
-            @RequestParam("tabType") String tabType) {
+            @RequestParam("tabType") String tabType,
+            @ApiParam(value = "券类型(不传:优惠券+代金券都返回,1:仅优惠券,4:仅代金券)") @RequestParam(value = "type", required = false) Integer type,
+            @ApiParam(value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(仅当type不为4时有效)") @RequestParam(value = "couponType", required = false) Integer couponType) {
         String authorization = request.getHeader("Authorization");
-        return diningCouponService.getUserCouponList(authorization, page, size, tabType);
+        return diningCouponService.getUserCouponList(authorization, page, size, tabType, type, couponType);
     }
 
     /**
@@ -73,18 +79,20 @@ public class DiningCouponController {
     /**
      * 获取该门店下用户可用/不可用优惠券列表(用于购物车/下单选券,按金额区分)
      */
-    @ApiOperation(value = "获取门店可用优惠券列表", notes = "需登录。按 storeId+amount 返回可用券与不可用券及原因(满减门槛等)")
+    @ApiOperation(value = "获取门店可用优惠券列表", notes = "需登录。按 storeId+amount 返回可用券与不可用券及原因(满减门槛等)。couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券")
     @GetMapping("/storeUsableList")
     @ApiImplicitParams({
             @ApiImplicitParam(name = "storeId", value = "门店id", dataType = "String", paramType = "query", required = true),
-            @ApiImplicitParam(name = "amount", value = "当前消费金额(满减门槛)", dataType = "BigDecimal", paramType = "query", required = true)
+            @ApiImplicitParam(name = "amount", value = "当前消费金额(满减门槛)", dataType = "BigDecimal", paramType = "query", required = true),
+            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)", dataType = "Integer", paramType = "query", required = false)
     })
     public R<Map<String, Object>> getStoreUserUsableCouponList(
             HttpServletRequest request,
             @RequestParam("storeId") String storeId,
-            @RequestParam("amount") BigDecimal amount) {
+            @RequestParam("amount") BigDecimal amount,
+            @ApiParam(value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券") @RequestParam(value = "couponType", required = false) Integer couponType) {
         String authorization = request.getHeader("Authorization");
-        return diningCouponService.getStoreUserUsableCouponList(authorization, storeId, amount);
+        return diningCouponService.getStoreUserUsableCouponList(authorization, storeId, amount, couponType);
     }
 
     /**
@@ -101,4 +109,68 @@ public class DiningCouponController {
             @ApiParam(value = "当前消费金额(可选,用于判断是否符合支付条件)") @RequestParam(required = false) BigDecimal amount) {
         return diningCouponService.getUserOwnedCoupons(storeId, amount);
     }
+
+    /**
+     * 获取该用户该店铺优惠券列表
+     */
+    @ApiOperation(value = "获取该用户该店铺优惠券列表", notes = "需登录,请求头带 Authorization。couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券")
+    @ApiOperationSupport(order = 7)
+    @GetMapping("/getStoreUserCouponList")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "商户id", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)", dataType = "Integer", paramType = "query", required = false)
+    })
+    public R<List<LifeDiscountCouponVo>> getStoreUserCouponList(
+            HttpServletRequest request,
+            @ApiParam(value = "商户id", required = true) @RequestParam("storeId") String storeId,
+            @ApiParam(value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券") @RequestParam(value = "couponType", required = false) Integer couponType) {
+        String authorization = request.getHeader("Authorization");
+        return diningCouponService.getStoreUserCouponList(authorization, storeId, couponType);
+    }
+
+    /**
+     * 查询用户目前所拥有的优惠券(可通过商铺ID进行查询)
+     */
+    @ApiOperation(value = "查询用户目前所拥有的优惠券", notes = "需登录。查询用户目前所拥有的优惠券(未使用且未过期),可通过商铺ID进行筛选。couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券")
+    @ApiOperationSupport(order = 8)
+    @GetMapping("/userOwnedByStore")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "商铺ID(可选,如果提供则只查询该商铺的优惠券)", dataType = "String", paramType = "query", required = false),
+            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)", dataType = "Integer", paramType = "query", required = false)
+    })
+    public R<List<LifeDiscountCouponVo>> getUserOwnedCouponsByStore(
+            @ApiParam(value = "商铺ID(可选)") @RequestParam(required = false) String storeId,
+            @ApiParam(value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券") @RequestParam(value = "couponType", required = false) Integer couponType) {
+        return diningCouponService.getUserOwnedCouponsByStore(storeId, couponType);
+    }
+
+    /**
+     * 获取该店铺所有优惠券(分页), 好友优惠券
+     */
+    @ApiOperation(value = "获取该店铺所有优惠券(分页)", notes = "需登录,请求头带 Authorization。couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券。tab: 0:全部,1:进行中,2:已结束,3:草稿,4:未开始,5:已下架,6:已清库")
+    @ApiOperationSupport(order = 10)
+    @GetMapping("/getStoreAllCouponList")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "page", value = "页码", dataType = "int", paramType = "query", required = false),
+            @ApiImplicitParam(name = "size", value = "每页条数", dataType = "int", paramType = "query", required = false),
+            @ApiImplicitParam(name = "storeId", value = "商户id", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "couponName", value = "优惠券名称", dataType = "String", paramType = "query", required = false),
+            @ApiImplicitParam(name = "tab", value = "分页类型(0:全部,1:进行中,2:已结束,3:草稿,4:未开始,5:已下架,6:已清库)", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "couponsFromType", value = "查询类型(1:我的优惠券,2:好友的优惠券)", dataType = "int", paramType = "query", required = false),
+            @ApiImplicitParam(name = "couponStatus", value = "优惠券状态(0:草稿,1:正式)", dataType = "int", paramType = "query", required = false),
+            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)", dataType = "Integer", paramType = "query", required = false)
+    })
+    public R<IPage<LifeDiscountCouponVo>> getStoreAllCouponList(
+            HttpServletRequest request,
+            @ApiParam(value = "页码", defaultValue = "1") @RequestParam(value = "page", defaultValue = "1") int page,
+            @ApiParam(value = "每页条数", defaultValue = "10") @RequestParam(value = "size", defaultValue = "10") int size,
+            @ApiParam(value = "商户id", required = true) @RequestParam("storeId") String storeId,
+            @ApiParam(value = "优惠券名称") @RequestParam(value = "couponName", required = false) String couponName,
+            @ApiParam(value = "分页类型(0:全部,1:进行中,2:已结束,3:草稿,4:未开始,5:已下架,6:已清库)", required = true) @RequestParam("tab") String tab,
+            @ApiParam(value = "查询类型(1:我的优惠券,2:好友的优惠券)", defaultValue = "1") @RequestParam(value = "couponsFromType", defaultValue = "1") int couponsFromType,
+            @ApiParam(value = "优惠券状态(0:草稿,1:正式)", defaultValue = "1") @RequestParam(value = "couponStatus", defaultValue = "1", required = false) int couponStatus,
+            @ApiParam(value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券") @RequestParam(value = "couponType", required = false) Integer couponType) {
+        String authorization = request.getHeader("Authorization");
+        return diningCouponService.getStoreAllCouponList(authorization, page, size, storeId, couponName, tab, couponsFromType, couponStatus, couponType);
+    }
 }

+ 60 - 0
alien-dining/src/main/java/shop/alien/dining/controller/DiningFileUploadController.java

@@ -0,0 +1,60 @@
+package shop.alien.dining.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import io.swagger.annotations.ApiSort;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.dining.feign.AlienStoreFeign;
+
+/**
+ * 点餐模块-文件上传控制器
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/XX
+ */
+@Slf4j
+@Api(tags = {"小程序-文件上传"})
+@ApiSort(10)
+@CrossOrigin
+@RestController
+@RequestMapping("/dining/file")
+@RequiredArgsConstructor
+public class DiningFileUploadController {
+
+    private final AlienStoreFeign alienStoreFeign;
+
+    /**
+     * 上传图片到OSS(单个文件上传,支持图片、视频或PDF)
+     *
+     * @param file 文件对象
+     * @return R.data 为文件路径(String)
+     */
+    @ApiOperation(value = "上传图片到OSS", notes = "支持图片、视频或PDF文件上传,返回OSS文件路径")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/upload")
+    public R<String> upload(@RequestParam("file") MultipartFile file) {
+        log.info("DiningFileUploadController.upload fileName:{}", file.getOriginalFilename());
+        try {
+            if (file == null || file.isEmpty()) {
+                return R.fail("文件不能为空");
+            }
+            R<String> result = alienStoreFeign.uploadFile(file);
+            if (result.isSuccess()) {
+                log.info("文件上传成功,OSS路径:{}", result.getData());
+                return result;
+            } else {
+                log.error("文件上传失败:{}", result.getMsg());
+                return R.fail("文件上传失败:" + result.getMsg());
+            }
+        } catch (Exception e) {
+            log.error("文件上传异常:{}", e.getMessage(), e);
+            return R.fail("文件上传异常:" + e.getMessage());
+        }
+    }
+}

+ 80 - 2
alien-dining/src/main/java/shop/alien/dining/feign/AlienStoreFeign.java

@@ -1,10 +1,17 @@
 package shop.alien.dining.feign;
 
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestHeader;
 import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.multipart.MultipartFile;
 import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeCollect;
 import shop.alien.entity.store.vo.LifeDiscountCouponVo;
 
 import java.math.BigDecimal;
@@ -45,6 +52,8 @@ public interface AlienStoreFeign {
      * @param page          页码
      * @param size          每页条数
      * @param tabType       0:全部(未使用),1:即将过期,2:已使用,3:已过期
+     * @param type          券类型(不传:优惠券+代金券都返回,1:仅优惠券查 life_discount_coupon,4:仅代金券查 life_coupon)(可选)
+     * @param couponType    优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选,仅当type不为4时有效)
      * @return R.data 为 List&lt;LifeDiscountCouponVo&gt;
      */
     @GetMapping("life-discount-coupon/getUserCouponList")
@@ -52,7 +61,9 @@ public interface AlienStoreFeign {
             @RequestHeader(value = "Authorization", required = false) String authorization,
             @RequestParam("page") int page,
             @RequestParam("size") int size,
-            @RequestParam("tabType") String tabType);
+            @RequestParam("tabType") String tabType,
+            @RequestParam(value = "type", required = false) Integer type,
+            @RequestParam(value = "couponType", required = false) Integer couponType);
 
     /**
      * 根据优惠券 ID 获取优惠券详情(含规则、门槛等)
@@ -68,15 +79,82 @@ public interface AlienStoreFeign {
 
     /**
      * 获取该门店下用户可用/不可用优惠券列表(按消费金额区分)
+     * couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券
      *
      * @param authorization 请求头 Authorization
      * @param storeId        门店 id
      * @param amount         当前消费金额(用于满减门槛判断)
+     * @param couponType     优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)
      * @return R.data 为 Map:canUseLifeDiscountCouponVos、forbidUseLifeDiscountCouponVos
      */
     @GetMapping("life-discount-coupon/getStoreUserUsableCouponList")
     R<Map<String, Object>> getStoreUserUsableCouponList(
             @RequestHeader(value = "Authorization", required = false) String authorization,
             @RequestParam("storeId") String storeId,
-            @RequestParam("amount") BigDecimal amount);
+            @RequestParam("amount") BigDecimal amount,
+            @RequestParam(value = "couponType", required = false) Integer couponType);
+
+    /**
+     * 获取该用户该店铺优惠券列表
+     * couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券
+     *
+     * @param authorization 请求头 Authorization,供 store 解析当前用户
+     * @param storeId       商户id
+     * @param couponType    优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)
+     * @return R.data 为 List&lt;LifeDiscountCouponVo&gt;
+     */
+    @GetMapping("life-discount-coupon/getStoreUserCouponList")
+    R<List<LifeDiscountCouponVo>> getStoreUserCouponList(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("storeId") String storeId,
+            @RequestParam(value = "couponType", required = false) Integer couponType);
+
+    /**
+     * 获取该店铺所有优惠券(分页), 好友优惠券
+     * couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券
+     *
+     * @param authorization   请求头 Authorization,供 store 解析当前用户
+     * @param page            页码(默认1)
+     * @param size            每页条数(默认10)
+     * @param storeId         商户id
+     * @param couponName      优惠券名称(可选)
+     * @param tab             分页类型(0:全部,1:进行中,2:已结束,3:草稿,4:未开始,5:已下架,6:已清库)
+     * @param couponsFromType 查询类型(1:我的优惠券,2:好友的优惠券)(默认1)
+     * @param couponStatus    优惠券状态(0:草稿,1:正式)(默认1)
+     * @param couponType      优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)
+     * @return R.data 为 Page&lt;LifeDiscountCouponVo&gt;(使用 Page 而不是 IPage,因为 Feign 需要具体类型进行反序列化)
+     */
+    @GetMapping("life-discount-coupon/getStoreAllCouponList")
+    R<Page<LifeDiscountCouponVo>> getStoreAllCouponList(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam(value = "page", defaultValue = "1") int page,
+            @RequestParam(value = "size", defaultValue = "10") int size,
+            @RequestParam("storeId") String storeId,
+            @RequestParam(value = "couponName", required = false) String couponName,
+            @RequestParam("tab") String tab,
+            @RequestParam(value = "couponsFromType", defaultValue = "1") int couponsFromType,
+            @RequestParam(value = "couponStatus", defaultValue = "1", required = false) int couponStatus,
+            @RequestParam(value = "couponType", required = false) Integer couponType);
+
+    // ==================== 收藏接口 ====================
+
+    /**
+     * 添加收藏
+     *
+     * @param lifeCollect 收藏对象
+     * @return R.data 为 Boolean(是否成功)
+     */
+    @PostMapping("/collect/addCollect")
+    R<Boolean> addCollect(@RequestBody LifeCollect lifeCollect);
+
+    // ==================== 文件上传接口 ====================
+
+    /**
+     * 上传图片到OSS(单个文件上传,支持图片、视频或PDF)
+     *
+     * @param file 文件对象
+     * @return R.data 为文件路径(String)
+     */
+    @PostMapping(value = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    R<String> uploadFile(@RequestPart("file") MultipartFile file);
 }

+ 22 - 0
alien-dining/src/main/java/shop/alien/dining/service/DiningCollectService.java

@@ -0,0 +1,22 @@
+package shop.alien.dining.service;
+
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeCollect;
+
+/**
+ * 点餐模块-收藏服务(Feign 调 store 收藏接口)
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/XX
+ */
+public interface DiningCollectService {
+
+    /**
+     * 添加收藏
+     *
+     * @param lifeCollect 收藏对象
+     * @return R.data 为 Boolean(是否成功)
+     */
+    R<Boolean> addCollect(LifeCollect lifeCollect);
+}

+ 46 - 2
alien-dining/src/main/java/shop/alien/dining/service/DiningCouponService.java

@@ -1,5 +1,6 @@
 package shop.alien.dining.service;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.vo.LifeDiscountCouponVo;
 
@@ -23,9 +24,11 @@ public interface DiningCouponService {
      * @param page          页码
      * @param size          每页条数
      * @param tabType       0:全部(未使用),1:即将过期,2:已使用,3:已过期
+     * @param type          券类型(不传:优惠券+代金券都返回,1:仅优惠券查 life_discount_coupon,4:仅代金券查 life_coupon)(可选)
+     * @param couponType    优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选,仅当type不为4时有效)
      * @return R.data 为 List&lt;LifeDiscountCouponVo&gt;
      */
-    R<List<LifeDiscountCouponVo>> getUserCouponList(String authorization, int page, int size, String tabType);
+    R<List<LifeDiscountCouponVo>> getUserCouponList(String authorization, int page, int size, String tabType, Integer type, Integer couponType);
 
     /**
      * 根据优惠券 id 获取优惠券详情
@@ -38,13 +41,15 @@ public interface DiningCouponService {
 
     /**
      * 获取该门店下用户可用/不可用优惠券列表(按消费金额区分,用于选券)
+     * couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券
      *
      * @param authorization 请求头 Authorization
      * @param storeId        门店 id
      * @param amount         当前消费金额(满减门槛)
+     * @param couponType     优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)
      * @return R.data 为 Map:canUseLifeDiscountCouponVos、forbidUseLifeDiscountCouponVos
      */
-    R<Map<String, Object>> getStoreUserUsableCouponList(String authorization, String storeId, BigDecimal amount);
+    R<Map<String, Object>> getStoreUserUsableCouponList(String authorization, String storeId, BigDecimal amount, Integer couponType);
 
     /**
      * 查询用户拥有的优惠券(按符合支付条件优先,其次优惠力度大的优先)
@@ -54,4 +59,43 @@ public interface DiningCouponService {
      * @return 优惠券列表
      */
     R<List<LifeDiscountCouponVo>> getUserOwnedCoupons(String storeId, BigDecimal amount);
+
+    /**
+     * 获取该用户该店铺优惠券列表
+     * couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券
+     *
+     * @param authorization 请求头 Authorization,透传至 store 解析用户
+     * @param storeId       商户id
+     * @param couponType    优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)
+     * @return R.data 为 List&lt;LifeDiscountCouponVo&gt;
+     */
+    R<List<LifeDiscountCouponVo>> getStoreUserCouponList(String authorization, String storeId, Integer couponType);
+
+    /**
+     * 获取该店铺所有优惠券(分页), 好友优惠券
+     * couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券
+     *
+     * @param authorization   请求头 Authorization,透传至 store 解析用户
+     * @param page            页码(默认1)
+     * @param size            每页条数(默认10)
+     * @param storeId         商户id
+     * @param couponName      优惠券名称(可选)
+     * @param tab             分页类型(0:全部,1:进行中,2:已结束,3:草稿,4:未开始,5:已下架,6:已清库)
+     * @param couponsFromType 查询类型(1:我的优惠券,2:好友的优惠券)(默认1)
+     * @param couponStatus    优惠券状态(0:草稿,1:正式)(默认1)
+     * @param couponType      优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)
+     * @return R.data 为 IPage&lt;LifeDiscountCouponVo&gt;
+     */
+    R<IPage<LifeDiscountCouponVo>> getStoreAllCouponList(String authorization, int page, int size, String storeId,
+                                                          String couponName, String tab, int couponsFromType,
+                                                          int couponStatus, Integer couponType);
+
+    /**
+     * 查询用户目前所拥有的优惠券(可通过商铺ID进行查询)
+     *
+     * @param storeId     商铺ID(可选,如果提供则只查询该商铺的优惠券)
+     * @param couponType  优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)
+     * @return 优惠券列表
+     */
+    R<List<LifeDiscountCouponVo>> getUserOwnedCouponsByStore(String storeId, Integer couponType);
 }

+ 39 - 0
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningCollectServiceImpl.java

@@ -0,0 +1,39 @@
+package shop.alien.dining.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import shop.alien.dining.feign.AlienStoreFeign;
+import shop.alien.dining.service.DiningCollectService;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeCollect;
+
+/**
+ * 点餐模块-收藏服务实现(透传 Feign 调 store)
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/XX
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DiningCollectServiceImpl implements DiningCollectService {
+
+    private final AlienStoreFeign alienStoreFeign;
+
+    @Override
+    public R<Boolean> addCollect(LifeCollect lifeCollect) {
+        log.info("DiningCollectService.addCollect lifeCollect={}", lifeCollect);
+        try {
+            R<Boolean> result = alienStoreFeign.addCollect(lifeCollect);
+            if (result == null) {
+                return R.fail("添加收藏失败");
+            }
+            return result;
+        } catch (Exception e) {
+            log.error("DiningCollectService.addCollect ERROR Msg={}", e.getMessage());
+            return R.fail("添加收藏失败");
+        }
+    }
+}

+ 251 - 12
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningCouponServiceImpl.java

@@ -1,6 +1,7 @@
 package shop.alien.dining.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -39,10 +40,10 @@ public class DiningCouponServiceImpl implements DiningCouponService {
     private final LifeDiscountCouponMapper lifeDiscountCouponMapper;
 
     @Override
-    public R<List<LifeDiscountCouponVo>> getUserCouponList(String authorization, int page, int size, String tabType) {
-        log.info("DiningCouponService.getUserCouponList page={}, size={}, tabType={}", page, size, tabType);
+    public R<List<LifeDiscountCouponVo>> getUserCouponList(String authorization, int page, int size, String tabType, Integer type, Integer couponType) {
+        log.info("DiningCouponService.getUserCouponList page={}, size={}, tabType={}, type={}, couponType={}", page, size, tabType, type, couponType);
         try {
-            R<List<LifeDiscountCouponVo>> result = alienStoreFeign.getUserCouponList(authorization, page, size, tabType);
+            R<List<LifeDiscountCouponVo>> result = alienStoreFeign.getUserCouponList(authorization, page, size, tabType, type, couponType);
             if (result == null) {
                 return R.fail("获取优惠券列表失败");
             }
@@ -69,10 +70,10 @@ public class DiningCouponServiceImpl implements DiningCouponService {
     }
 
     @Override
-    public R<Map<String, Object>> getStoreUserUsableCouponList(String authorization, String storeId, BigDecimal amount) {
-        log.info("DiningCouponService.getStoreUserUsableCouponList storeId={}, amount={}", storeId, amount);
+    public R<Map<String, Object>> getStoreUserUsableCouponList(String authorization, String storeId, BigDecimal amount, Integer couponType) {
+        log.info("DiningCouponService.getStoreUserUsableCouponList storeId={}, amount={}, couponType={}", storeId, amount, couponType);
         try {
-            R<Map<String, Object>> result = alienStoreFeign.getStoreUserUsableCouponList(authorization, storeId, amount);
+            R<Map<String, Object>> result = alienStoreFeign.getStoreUserUsableCouponList(authorization, storeId, amount, couponType);
             if (result == null) {
                 return R.fail("获取门店可用优惠券列表失败");
             }
@@ -126,7 +127,8 @@ public class DiningCouponServiceImpl implements DiningCouponService {
                 return R.data(new ArrayList<>());
             }
 
-            // 转换为VO并设置用户券ID
+            // 转换为VO并设置用户券ID,同时过滤掉已过期和未在使用时间内的优惠券
+            LocalDate now = LocalDate.now();
             List<LifeDiscountCouponVo> couponVos = new ArrayList<>();
             for (LifeDiscountCoupon coupon : coupons) {
                 // 找到对应的用户券
@@ -139,6 +141,39 @@ public class DiningCouponServiceImpl implements DiningCouponService {
                     continue;
                 }
 
+                // 过滤1:检查用户券的过期时间(expirationTime)
+                if (userCoupon.getExpirationTime() != null && now.isAfter(userCoupon.getExpirationTime())) {
+                    log.debug("过滤已过期的用户券, couponId={}, expirationTime={}, now={}", 
+                            coupon.getId(), userCoupon.getExpirationTime(), now);
+                    continue;
+                }
+
+                // 过滤2:检查优惠券的使用时间范围(startDate 和 endDate)
+                // 如果优惠券有设置使用时间范围,则检查当前日期是否在范围内
+                if (coupon.getStartDate() != null || coupon.getEndDate() != null) {
+                    boolean inTimeRange = true;
+                    if (coupon.getStartDate() != null && now.isBefore(coupon.getStartDate())) {
+                        // 当前日期早于开始日期,未在使用时间内
+                        inTimeRange = false;
+                    }
+                    if (coupon.getEndDate() != null && now.isAfter(coupon.getEndDate())) {
+                        // 当前日期晚于结束日期,未在使用时间内
+                        inTimeRange = false;
+                    }
+                    if (!inTimeRange) {
+                        log.debug("过滤未在使用时间内的优惠券, couponId={}, startDate={}, endDate={}, now={}", 
+                                coupon.getId(), coupon.getStartDate(), coupon.getEndDate(), now);
+                        continue;
+                    }
+                }
+
+                // 过滤3:检查优惠券的有效期(validDate)
+                if (coupon.getValidDate() != null && now.isAfter(coupon.getValidDate())) {
+                    log.debug("过滤已过期的优惠券(validDate), couponId={}, validDate={}, now={}", 
+                            coupon.getId(), coupon.getValidDate(), now);
+                    continue;
+                }
+
                 LifeDiscountCouponVo vo = convertToVo(coupon, userCoupon);
                 couponVos.add(vo);
             }
@@ -160,13 +195,14 @@ public class DiningCouponServiceImpl implements DiningCouponService {
                         return 1; // vo2 排在前面
                     }
 
-                    // 第二优先级:优惠力度大的优先(nominalValue 大的
-                    BigDecimal nominalValue1 = vo1.getNominalValue() != null ? vo1.getNominalValue() : BigDecimal.ZERO;
-                    BigDecimal nominalValue2 = vo2.getNominalValue() != null ? vo2.getNominalValue() : BigDecimal.ZERO;
-                    return nominalValue2.compareTo(nominalValue1); // 降序排列
+                    // 第二优先级:优惠力度大的优先(根据优惠券类型计算实际优惠金额
+                    BigDecimal discountAmount1 = calculateDiscountAmountForVO(vo1, amount);
+                    BigDecimal discountAmount2 = calculateDiscountAmountForVO(vo2, amount);
+                    return discountAmount2.compareTo(discountAmount1); // 降序排列
                 });
             } else {
-                // 如果没有提供金额,只按优惠力度排序
+                // 如果没有提供金额,无法计算折扣券的实际优惠金额,只按面值排序(满减券)
+                // 注意:这种情况下折扣券无法准确排序,建议前端传入订单金额
                 couponVos.sort((vo1, vo2) -> {
                     BigDecimal nominalValue1 = vo1.getNominalValue() != null ? vo1.getNominalValue() : BigDecimal.ZERO;
                     BigDecimal nominalValue2 = vo2.getNominalValue() != null ? vo2.getNominalValue() : BigDecimal.ZERO;
@@ -183,6 +219,165 @@ public class DiningCouponServiceImpl implements DiningCouponService {
         }
     }
 
+    @Override
+    public R<List<LifeDiscountCouponVo>> getStoreUserCouponList(String authorization, String storeId, Integer couponType) {
+        log.info("DiningCouponService.getStoreUserCouponList storeId={}, couponType={}", storeId, couponType);
+        try {
+            R<List<LifeDiscountCouponVo>> result = alienStoreFeign.getStoreUserCouponList(authorization, storeId, couponType);
+            if (result == null) {
+                return R.fail("获取该用户该店铺优惠券列表失败");
+            }
+            return result;
+        } catch (Exception e) {
+            log.error("DiningCouponService.getStoreUserCouponList ERROR Msg={}", e.getMessage());
+            return R.fail("获取该用户该店铺优惠券列表失败");
+        }
+    }
+
+    @Override
+    public R<IPage<LifeDiscountCouponVo>> getStoreAllCouponList(String authorization, int page, int size, String storeId,
+                                                                String couponName, String tab, int couponsFromType,
+                                                                int couponStatus, Integer couponType) {
+        log.info("DiningCouponService.getStoreAllCouponList storeId={}, page={}, size={}, couponName={}, tab={}, couponsFromType={}, couponStatus={}, couponType={}",
+                storeId, page, size, couponName, tab, couponsFromType, couponStatus, couponType);
+        try {
+            // Feign 返回 Page 类型(具体实现类),但 Service 接口使用 IPage(接口类型)
+            // Page 实现了 IPage,所以可以直接返回
+            R<com.baomidou.mybatisplus.extension.plugins.pagination.Page<LifeDiscountCouponVo>> result = alienStoreFeign.getStoreAllCouponList(
+                    authorization, page, size, storeId, couponName, tab, couponsFromType, couponStatus, couponType);
+            if (result == null) {
+                return R.fail("获取该店铺所有优惠券列表失败");
+            }
+            // Page 实现了 IPage 接口,可以直接转换
+            return R.data(result.getData());
+        } catch (Exception e) {
+            log.error("DiningCouponService.getStoreAllCouponList ERROR Msg={}", e.getMessage(), e);
+            return R.fail("获取该店铺所有优惠券列表失败");
+        }
+    }
+
+    @Override
+    public R<List<LifeDiscountCouponVo>> getUserOwnedCouponsByStore(String storeId, Integer couponType) {
+        log.info("查询用户目前所拥有的优惠券, storeId={}, couponType={}", storeId, couponType);
+
+        try {
+            // 获取当前用户ID
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+
+            // 查询用户拥有的优惠券(未使用且未过期的)
+            LambdaQueryWrapper<LifeDiscountCouponUser> userWrapper = new LambdaQueryWrapper<>();
+            userWrapper.eq(LifeDiscountCouponUser::getUserId, userId);
+            userWrapper.eq(LifeDiscountCouponUser::getStatus, 0); // 0:待使用
+            userWrapper.eq(LifeDiscountCouponUser::getDeleteFlag, 0);
+            userWrapper.ge(LifeDiscountCouponUser::getExpirationTime, LocalDate.now()); // 未过期
+            List<LifeDiscountCouponUser> userCoupons = lifeDiscountCouponUserMapper.selectList(userWrapper);
+
+            if (userCoupons == null || userCoupons.isEmpty()) {
+                log.info("用户没有可用的优惠券, userId={}", userId);
+                return R.data(new ArrayList<>());
+            }
+
+            // 获取优惠券ID列表
+            List<Integer> couponIds = userCoupons.stream()
+                    .map(LifeDiscountCouponUser::getCouponId)
+                    .collect(Collectors.toList());
+
+            // 查询优惠券详情
+            LambdaQueryWrapper<LifeDiscountCoupon> couponWrapper = new LambdaQueryWrapper<>();
+            couponWrapper.in(LifeDiscountCoupon::getId, couponIds);
+            couponWrapper.eq(LifeDiscountCoupon::getDeleteFlag, 0);
+            // 如果提供了商铺ID,则只查询该商铺的优惠券
+            if (StringUtils.hasText(storeId)) {
+                couponWrapper.eq(LifeDiscountCoupon::getStoreId, storeId);
+            }
+            // 如果提供了优惠券类型,则只查询该类型的优惠券
+            if (couponType != null) {
+                couponWrapper.eq(LifeDiscountCoupon::getCouponType, couponType);
+            }
+            List<LifeDiscountCoupon> coupons = lifeDiscountCouponMapper.selectList(couponWrapper);
+
+            if (coupons == null || coupons.isEmpty()) {
+                log.info("未找到优惠券详情, userId={}, storeId={}, couponIds={}", userId, storeId, couponIds);
+                return R.data(new ArrayList<>());
+            }
+
+            // 转换为VO并设置用户券ID,同时过滤掉已过期和未在使用时间内的优惠券
+            LocalDate now = LocalDate.now();
+            List<LifeDiscountCouponVo> couponVos = new ArrayList<>();
+            for (LifeDiscountCoupon coupon : coupons) {
+                // 找到对应的用户券
+                LifeDiscountCouponUser userCoupon = userCoupons.stream()
+                        .filter(uc -> uc.getCouponId().equals(coupon.getId()))
+                        .findFirst()
+                        .orElse(null);
+
+                if (userCoupon == null) {
+                    continue;
+                }
+
+                // 过滤1:检查用户券的过期时间(expirationTime)
+                if (userCoupon.getExpirationTime() != null && now.isAfter(userCoupon.getExpirationTime())) {
+                    log.debug("过滤已过期的用户券, couponId={}, expirationTime={}, now={}", 
+                            coupon.getId(), userCoupon.getExpirationTime(), now);
+                    continue;
+                }
+
+                // 过滤2:检查优惠券的使用时间范围(startDate 和 endDate)
+                // 如果优惠券有设置使用时间范围,则检查当前日期是否在范围内
+                if (coupon.getStartDate() != null || coupon.getEndDate() != null) {
+                    boolean inTimeRange = true;
+                    if (coupon.getStartDate() != null && now.isBefore(coupon.getStartDate())) {
+                        // 当前日期早于开始日期,未在使用时间内
+                        inTimeRange = false;
+                    }
+                    if (coupon.getEndDate() != null && now.isAfter(coupon.getEndDate())) {
+                        // 当前日期晚于结束日期,未在使用时间内
+                        inTimeRange = false;
+                    }
+                    if (!inTimeRange) {
+                        log.debug("过滤未在使用时间内的优惠券, couponId={}, startDate={}, endDate={}, now={}", 
+                                coupon.getId(), coupon.getStartDate(), coupon.getEndDate(), now);
+                        continue;
+                    }
+                }
+
+                // 过滤3:检查优惠券的有效期(validDate)
+                if (coupon.getValidDate() != null && now.isAfter(coupon.getValidDate())) {
+                    log.debug("过滤已过期的优惠券(validDate), couponId={}, validDate={}, now={}", 
+                            coupon.getId(), coupon.getValidDate(), now);
+                    continue;
+                }
+
+                LifeDiscountCouponVo vo = convertToVo(coupon, userCoupon);
+                couponVos.add(vo);
+            }
+
+            // 按创建时间倒序排列(最新的在前)
+            couponVos.sort((vo1, vo2) -> {
+                if (vo1.getCreatedTime() == null && vo2.getCreatedTime() == null) {
+                    return 0;
+                }
+                if (vo1.getCreatedTime() == null) {
+                    return 1;
+                }
+                if (vo2.getCreatedTime() == null) {
+                    return -1;
+                }
+                return vo2.getCreatedTime().compareTo(vo1.getCreatedTime()); // 降序排列
+            });
+
+            log.info("查询用户拥有的优惠券成功, userId={}, storeId={}, count={}", userId, storeId, couponVos.size());
+            return R.data(couponVos);
+
+        } catch (Exception e) {
+            log.error("查询用户拥有的优惠券失败: {}", e.getMessage(), e);
+            return R.fail("查询用户拥有的优惠券失败: " + e.getMessage());
+        }
+    }
+
     /**
      * 将优惠券实体转换为VO
      */
@@ -211,4 +406,48 @@ public class DiningCouponServiceImpl implements DiningCouponService {
         vo.setDiscountRate(coupon.getDiscountRate());
         return vo;
     }
+
+    /**
+     * 计算优惠券的实际优惠金额(用于排序)
+     * 根据优惠券类型(满减券或折扣券)计算实际优惠金额
+     *
+     * @param vo          优惠券VO
+     * @param orderAmount 订单金额(用于计算折扣券的优惠金额)
+     * @return 实际优惠金额
+     */
+    private BigDecimal calculateDiscountAmountForVO(LifeDiscountCouponVo vo, BigDecimal orderAmount) {
+        if (vo == null) {
+            return BigDecimal.ZERO;
+        }
+
+        Integer couponType = vo.getCouponType();
+        BigDecimal discountAmount = BigDecimal.ZERO;
+
+        if (couponType != null && couponType == 2) {
+            // 折扣券:根据折扣率计算优惠金额
+            // discountRate: 0-100,例如80表示8折,优惠金额 = 订单金额 * (100 - discountRate) / 100
+            BigDecimal discountRate = vo.getDiscountRate();
+            if (discountRate != null && discountRate.compareTo(BigDecimal.ZERO) > 0 
+                    && discountRate.compareTo(new BigDecimal(100)) <= 0 && orderAmount != null 
+                    && orderAmount.compareTo(BigDecimal.ZERO) > 0) {
+                // 计算折扣后的金额
+                BigDecimal discountedAmount = orderAmount.multiply(discountRate)
+                        .divide(new BigDecimal(100), 2, BigDecimal.ROUND_HALF_UP);
+                // 优惠金额 = 原价 - 折扣后价格
+                discountAmount = orderAmount.subtract(discountedAmount);
+            }
+        } else {
+            // 满减券(默认或couponType=1):使用nominalValue
+            discountAmount = vo.getNominalValue();
+            if (discountAmount == null) {
+                discountAmount = BigDecimal.ZERO;
+            }
+            // 优惠金额不能超过订单总金额
+            if (orderAmount != null && discountAmount.compareTo(orderAmount) > 0) {
+                discountAmount = orderAmount;
+            }
+        }
+
+        return discountAmount;
+    }
 }

+ 63 - 3
alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java

@@ -164,6 +164,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         StoreOrder order = null;
         String orderNo = null;
         boolean isUpdate = false; // 是否是更新订单
+        boolean isFirstAddDish = false; // 是否是首次加餐(首次订单发生变化)
         
         // 检查桌号是否已绑定订单
         if (table.getCurrentOrderId() != null) {
@@ -176,6 +177,15 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
                 orderNo = order.getOrderNo(); // 使用原订单号
                 log.info("桌号已绑定订单,更新订单信息, orderId={}, orderNo={}", order.getId(), orderNo);
                 
+                // 在更新订单之前,检查订单明细中是否已经有 is_add_dish=1 的记录
+                // 如果没有,说明这是首次加餐(首次订单发生变化)
+                LambdaQueryWrapper<StoreOrderDetail> checkBeforeAddDishWrapper = new LambdaQueryWrapper<>();
+                checkBeforeAddDishWrapper.eq(StoreOrderDetail::getOrderId, order.getId())
+                        .eq(StoreOrderDetail::getDeleteFlag, 0)
+                        .eq(StoreOrderDetail::getIsAddDish, 1);
+                Integer addDishCountBefore = orderDetailMapper.selectCount(checkBeforeAddDishWrapper);
+                isFirstAddDish = (addDishCountBefore == null || addDishCountBefore == 0);
+                
                 // 更新订单信息
                 order.setDinerCount(dto.getDinerCount());
                 order.setContactPhone(dto.getContactPhone());
@@ -250,6 +260,8 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         }
         final StoreOrder finalOrder = order;
         final String finalOrderNo = orderNo;
+        final boolean finalIsUpdate = isUpdate; // 用于 lambda 表达式
+        final boolean finalIsFirstAddDish = isFirstAddDish; // 用于后续判断
 
         // 更新优惠券使用记录状态为已下单
         if (dto.getCouponId() != null) {
@@ -272,6 +284,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         }
 
         // 创建订单明细
+        // 如果是更新订单,需要判断哪些商品是新增的或数量增加的,标记为加餐
         List<StoreOrderDetail> orderDetails = cart.getItems().stream()
                 .map(item -> {
                     StoreOrderDetail detail = new StoreOrderDetail();
@@ -290,6 +303,18 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
                     detail.setCreatedUserId(userId);
                     detail.setCreatedTime(now);
                     detail.setUpdatedTime(now);
+                    
+                    // 如果是更新订单,判断是否为新增商品或数量增加的商品
+                    if (finalIsUpdate) {
+                        Integer lockedQuantity = item.getLockedQuantity();
+                        // 如果是新增商品(lockedQuantity 为 null 或 0)或数量增加的商品,标记为加餐
+                        if (lockedQuantity == null || lockedQuantity == 0 || 
+                            (item.getQuantity() != null && item.getQuantity() > lockedQuantity)) {
+                            detail.setIsAddDish(1); // 标记为加餐
+                            detail.setAddDishTime(now);
+                        }
+                    }
+                    
                     return detail;
                 })
                 .collect(Collectors.toList());
@@ -299,13 +324,23 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         }
 
         // 记录订单变更日志
-        recordOrderChangeLog(finalOrder.getId(), finalOrderNo, cart.getItems(), isUpdate ? 3 : 1, now, userId, userPhone);
+        recordOrderChangeLog(finalOrder.getId(), finalOrderNo, cart.getItems(), finalIsUpdate ? 3 : 1, now, userId, userPhone);
 
-        // 更新桌号的当前订单ID(如果是新订单才需要更新,更新订单时已经绑定了)
-        if (!isUpdate) {
+        // 更新桌号的当前订单ID和状态
+        if (!finalIsUpdate) {
+            // 新订单:设置订单ID,状态为就餐中(1)
             table.setCurrentOrderId(finalOrder.getId());
             table.setStatus(1); // 就餐中
             storeTableMapper.updateById(table);
+        } else if (finalIsFirstAddDish) {
+            // 更新订单且是首次加餐:更新餐桌状态为加餐状态(3)
+            table.setStatus(3); // 加餐状态
+            table.setUpdatedTime(now);
+            if (userId != null) {
+                table.setUpdatedUserId(userId);
+            }
+            storeTableMapper.updateById(table);
+            log.info("首次加餐(通过更新订单),更新餐桌状态为加餐状态, tableId={}, orderId={}", table.getId(), finalOrder.getId());
         }
 
         // 锁定购物车商品数量(禁止减少或删除已下单的商品)
@@ -619,6 +654,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
                         item.setCuisineName(detail.getCuisineName());
                         item.setCuisineImage(detail.getCuisineImage());
                         item.setQuantity(detail.getQuantity());
+                        item.setUnitPrice(detail.getUnitPrice());
                         return item;
                     })
                     .collect(Collectors.toList());
@@ -663,6 +699,15 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         Integer userId = TokenUtil.getCurrentUserId();
         String userPhone = TokenUtil.getCurrentUserPhone();
 
+        // 在加餐之前,检查订单明细中是否已经有 is_add_dish=1 的记录
+        // 如果没有,说明这是首次加餐(首次订单发生变化)
+        LambdaQueryWrapper<StoreOrderDetail> checkBeforeAddDishWrapper = new LambdaQueryWrapper<>();
+        checkBeforeAddDishWrapper.eq(StoreOrderDetail::getOrderId, orderId)
+                .eq(StoreOrderDetail::getDeleteFlag, 0)
+                .eq(StoreOrderDetail::getIsAddDish, 1);
+        Integer addDishCountBefore = orderDetailMapper.selectCount(checkBeforeAddDishWrapper);
+        boolean isFirstAddDish = (addDishCountBefore == null || addDishCountBefore == 0);
+
         // 查询是否已有该菜品
         LambdaQueryWrapper<StoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
         detailWrapper.eq(StoreOrderDetail::getOrderId, orderId);
@@ -750,6 +795,20 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         
         recordOrderChangeLog(orderId, order.getOrderNo(), addDishItems, 2, now, userId, userPhone);
 
+        // 如果是首次加餐(首次订单发生变化),更新餐桌状态为加餐状态(3)
+        if (isFirstAddDish) {
+            StoreTable table = storeTableMapper.selectById(order.getTableId());
+            if (table != null) {
+                table.setStatus(3); // 加餐状态
+                table.setUpdatedTime(new Date());
+                if (userId != null) {
+                    table.setUpdatedUserId(userId);
+                }
+                storeTableMapper.updateById(table);
+                log.info("首次加餐,更新餐桌状态为加餐状态, tableId={}, orderId={}", table.getId(), orderId);
+            }
+        }
+
         log.info("加餐成功, orderId={}", orderId);
         return order;
     }
@@ -1149,6 +1208,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
                         item.setCuisineName(detail.getCuisineName());
                         item.setCuisineImage(detail.getCuisineImage());
                         item.setQuantity(detail.getQuantity());
+                        item.setUnitPrice(detail.getUnitPrice());
                         return item;
                     })
                     .collect(Collectors.toList());

+ 1 - 1
alien-entity/src/main/java/shop/alien/entity/store/StoreTable.java

@@ -53,7 +53,7 @@ public class StoreTable {
     @TableField("qrcode_url")
     private String qrcodeUrl;
 
-    @ApiModelProperty(value = "状态(0:空闲, 1:就餐中, 2:其他)")
+    @ApiModelProperty(value = "状态(0:空闲, 1:就餐中, 2:其他, 3:加餐)")
     @TableField("status")
     private Integer status;
 

+ 5 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/OrderCuisineItemVO.java

@@ -4,6 +4,8 @@ import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
+import java.math.BigDecimal;
+
 /**
  * 订单菜品项VO
  *
@@ -25,4 +27,7 @@ public class OrderCuisineItemVO {
 
     @ApiModelProperty(value = "数量")
     private Integer quantity;
+
+    @ApiModelProperty(value = "单价")
+    private BigDecimal unitPrice;
 }

+ 12 - 6
alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/LifeDiscountCouponPlatformServiceImpl.java

@@ -69,6 +69,13 @@ public class LifeDiscountCouponPlatformServiceImpl extends ServiceImpl<LifeDisco
             //发布优惠券表信息
             LifeDiscountCoupon lifeDiscountCoupon = new LifeDiscountCoupon();
             BeanUtils.copyProperties(lifeDiscountCouponDto, lifeDiscountCoupon);
+            // 显式设置最低消费金额(门槛费),确保能正确保存
+            // 如果 DTO 中有值,使用 DTO 的值;如果为 null,设置为 0(表示无门槛)
+            if (lifeDiscountCouponDto.getMinimumSpendingAmount() != null) {
+                lifeDiscountCoupon.setMinimumSpendingAmount(lifeDiscountCouponDto.getMinimumSpendingAmount());
+            } else {
+                lifeDiscountCoupon.setMinimumSpendingAmount(BigDecimal.ZERO);
+            }
             // 根据开始领取时间判断可领取状态
             // 判断是否在领取时间内
 
@@ -98,10 +105,6 @@ public class LifeDiscountCouponPlatformServiceImpl extends ServiceImpl<LifeDisco
                 }
             }
 
-            if (lifeDiscountCoupon.getMinimumSpendingAmount() == null) {
-                lifeDiscountCoupon.setMinimumSpendingAmount(BigDecimal.valueOf(0));
-            }
-
             lifeDiscountCouponMapper.insert(lifeDiscountCoupon);
             //发布优惠券规则信息
             //周中规则保存
@@ -177,8 +180,11 @@ public class LifeDiscountCouponPlatformServiceImpl extends ServiceImpl<LifeDisco
             LifeDiscountCoupon lifeDiscountCoupon = new LifeDiscountCoupon();
             lifeDiscountCoupon.setId(Integer.parseInt(lifeDiscountCouponDto.getCouponId()));
             BeanUtils.copyProperties(lifeDiscountCouponDto, lifeDiscountCoupon);
-            // 如果最低消费为null,设置为0(表示无门槛)
-            if (lifeDiscountCoupon.getMinimumSpendingAmount() == null) {
+            // 显式设置最低消费金额(门槛费),确保能正确保存
+            // 如果 DTO 中有值,使用 DTO 的值;如果为 null,设置为 0(表示无门槛)
+            if (lifeDiscountCouponDto.getMinimumSpendingAmount() != null) {
+                lifeDiscountCoupon.setMinimumSpendingAmount(lifeDiscountCouponDto.getMinimumSpendingAmount());
+            } else {
                 lifeDiscountCoupon.setMinimumSpendingAmount(BigDecimal.ZERO);
             }
 

+ 283 - 0
alien-store/src/main/java/shop/alien/store/controller/DiningServiceController.java

@@ -0,0 +1,283 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreTable;
+import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.entity.store.vo.*;
+import shop.alien.store.feign.DiningServiceFeign;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+
+/**
+ * 点餐服务 Controller
+ * 供前端调用 alien-dining 模块的接口
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/XX
+ */
+@Slf4j
+@Api(tags = {"点餐服务接口"})
+@ApiSort(20)
+@CrossOrigin
+@RestController
+@RequestMapping("/dining")
+@RequiredArgsConstructor
+public class DiningServiceController {
+
+    private final DiningServiceFeign diningServiceFeign;
+
+    /**
+     * 从请求头获取 Authorization token
+     */
+    private String getAuthorization(HttpServletRequest request) {
+        String authorization = request.getHeader("Authorization");
+        if (authorization == null || authorization.isEmpty()) {
+            authorization = request.getHeader("authorization");
+        }
+        return authorization;
+    }
+
+    // ==================== 点餐相关接口 ====================
+
+    @ApiOperation(value = "获取点餐页面信息", notes = "获取店铺名称、桌号、就餐人数等信息")
+    @ApiOperationSupport(order = 1)
+    @GetMapping("/page-info")
+    public R<DiningPageInfoVO> getDiningPageInfo(
+            HttpServletRequest request,
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "就餐人数", required = true) @RequestParam Integer dinerCount) {
+        try {
+            String authorization = getAuthorization(request);
+            log.info("获取点餐页面信息: tableId={}, dinerCount={}", tableId, dinerCount);
+            return diningServiceFeign.getDiningPageInfo(authorization, tableId, dinerCount);
+        } catch (Exception e) {
+            log.error("获取点餐页面信息失败: {}", e.getMessage(), e);
+            return R.fail("获取点餐页面信息失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "搜索菜品", notes = "根据关键词搜索菜品")
+    @ApiOperationSupport(order = 2)
+    @GetMapping("/search")
+    public R<List<CuisineListVO>> searchCuisines(
+            HttpServletRequest request,
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId,
+            @ApiParam(value = "搜索关键词", required = true) @RequestParam String keyword,
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
+        try {
+            String authorization = getAuthorization(request);
+            log.info("搜索菜品: storeId={}, keyword={}, tableId={}", storeId, keyword, tableId);
+            return diningServiceFeign.searchCuisines(authorization, storeId, keyword, tableId);
+        } catch (Exception e) {
+            log.error("搜索菜品失败: {}", e.getMessage(), e);
+            return R.fail("搜索菜品失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "根据分类获取菜品列表", notes = "根据分类ID获取菜品列表,支持分页")
+    @ApiOperationSupport(order = 3)
+    @GetMapping("/cuisines")
+    public R<List<CuisineListVO>> getCuisinesByCategory(
+            HttpServletRequest request,
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId,
+            @ApiParam(value = "分类ID(可为空,查询所有)") @RequestParam(required = false) Integer categoryId,
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "页码", defaultValue = "1") @RequestParam(defaultValue = "1") Integer page,
+            @ApiParam(value = "每页数量", defaultValue = "12") @RequestParam(defaultValue = "12") Integer size) {
+        try {
+            String authorization = getAuthorization(request);
+            log.info("根据分类获取菜品列表: storeId={}, categoryId={}, tableId={}, page={}, size={}",
+                    storeId, categoryId, tableId, page, size);
+            return diningServiceFeign.getCuisinesByCategory(authorization, storeId, categoryId, tableId, page, size);
+        } catch (Exception e) {
+            log.error("根据分类获取菜品列表失败: {}", e.getMessage(), e);
+            return R.fail("根据分类获取菜品列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "获取菜品详情", notes = "获取菜品详细信息,包含图片列表、月售数量等")
+    @ApiOperationSupport(order = 4)
+    @GetMapping("/cuisine/{cuisineId}")
+    public R<CuisineDetailVO> getCuisineDetail(
+            HttpServletRequest request,
+            @ApiParam(value = "菜品ID", required = true) @PathVariable Integer cuisineId,
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
+        try {
+            String authorization = getAuthorization(request);
+            log.info("获取菜品详情: cuisineId={}, tableId={}", cuisineId, tableId);
+            return diningServiceFeign.getCuisineDetail(authorization, cuisineId, tableId);
+        } catch (Exception e) {
+            log.error("获取菜品详情失败: {}", e.getMessage(), e);
+            return R.fail("获取菜品详情失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "获取可领取的优惠券列表", notes = "获取用户可领取的优惠券")
+    @ApiOperationSupport(order = 5)
+    @GetMapping("/coupons/available")
+    public R<List<AvailableCouponVO>> getAvailableCoupons(
+            HttpServletRequest request,
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId) {
+        try {
+            String authorization = getAuthorization(request);
+            log.info("获取可领取的优惠券列表: storeId={}", storeId);
+            return diningServiceFeign.getAvailableCoupons(authorization, storeId);
+        } catch (Exception e) {
+            log.error("获取可领取的优惠券列表失败: {}", e.getMessage(), e);
+            return R.fail("获取可领取的优惠券列表失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 订单相关接口 ====================
+
+    @ApiOperation(value = "获取购物车", notes = "根据桌号ID获取购物车信息")
+    @ApiOperationSupport(order = 6)
+    @GetMapping("/order/cart/{tableId}")
+    public R<CartDTO> getCart(
+            HttpServletRequest request,
+            @ApiParam(value = "桌号ID", required = true) @PathVariable Integer tableId) {
+        try {
+            String authorization = getAuthorization(request);
+            log.info("获取购物车: tableId={}", tableId);
+            return diningServiceFeign.getCart(authorization, tableId);
+        } catch (Exception e) {
+            log.error("获取购物车失败: {}", e.getMessage(), e);
+            return R.fail("获取购物车失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "获取订单详情", notes = "根据订单ID获取订单详细信息")
+    @ApiOperationSupport(order = 7)
+    @GetMapping("/order/{orderId}")
+    public R<OrderDetailWithChangeLogVO> getOrderDetail(
+            HttpServletRequest request,
+            @ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        try {
+            String authorization = getAuthorization(request);
+            log.info("获取订单详情: orderId={}", orderId);
+            return diningServiceFeign.getOrderDetail(authorization, orderId);
+        } catch (Exception e) {
+            log.error("获取订单详情失败: {}", e.getMessage(), e);
+            return R.fail("获取订单详情失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "获取订单列表", notes = "分页获取订单列表,支持按状态筛选")
+    @ApiOperationSupport(order = 8)
+    @GetMapping("/order/list")
+    public R<IPage<StoreOrderPageVO>> getOrderList(
+            HttpServletRequest request,
+            @ApiParam(value = "页码", defaultValue = "1") @RequestParam(defaultValue = "1") Integer page,
+            @ApiParam(value = "每页数量", defaultValue = "10") @RequestParam(defaultValue = "10") Integer size,
+            @ApiParam(value = "订单状态(可选)") @RequestParam(required = false) Integer status) {
+        try {
+            String authorization = getAuthorization(request);
+            log.info("获取订单列表: page={}, size={}, status={}", page, size, status);
+            return diningServiceFeign.getOrderList(authorization, page, size, status);
+        } catch (Exception e) {
+            log.error("获取订单列表失败: {}", e.getMessage(), e);
+            return R.fail("获取订单列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "分页查询订单列表", notes = "分页查询订单列表,包含订单中的菜品数量、菜品名称、菜品图片。支持按订单编号或菜品名称搜索(限15字)")
+    @ApiOperationSupport(order = 8)
+    @GetMapping("/order/page")
+    public R<IPage<StoreOrderPageVO>> getOrderPage(
+            HttpServletRequest request,
+            @ApiParam(value = "页码", defaultValue = "1") @RequestParam(defaultValue = "1") Long current,
+            @ApiParam(value = "每页数量", defaultValue = "10") @RequestParam(defaultValue = "10") Long size,
+            @ApiParam(value = "门店ID") @RequestParam(required = false) Integer storeId,
+            @ApiParam(value = "桌号ID") @RequestParam(required = false) Integer tableId,
+            @ApiParam(value = "订单状态(0:待支付, 1:已支付, 2:已取消, 3:已完成)") @RequestParam(required = false) Integer orderStatus,
+            @ApiParam(value = "搜索关键词(订单编号或菜品名称,限15字)") @RequestParam(required = false) String keyword) {
+        try {
+            String authorization = getAuthorization(request);
+            log.info("分页查询订单列表: current={}, size={}, storeId={}, tableId={}, orderStatus={}, keyword={}",
+                    current, size, storeId, tableId, orderStatus, keyword);
+            R<Page<StoreOrderPageVO>> result = diningServiceFeign.getOrderPage(authorization, current, size, storeId, tableId, orderStatus, keyword);
+            // 将 Page 转换为 IPage 返回(Page 实现了 IPage 接口,可以直接返回)
+            return R.data(result.getData());
+        } catch (Exception e) {
+            log.error("分页查询订单列表失败: {}", e.getMessage(), e);
+            return R.fail("分页查询订单列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "查询订单变更记录", notes = "根据订单ID查询订单的所有变更记录(按批次分组),用于展示每次下单/加餐都加了什么商品")
+    @ApiOperationSupport(order = 9)
+    @GetMapping("/order/change-log/{orderId}")
+    public R<List<OrderChangeLogBatchVO>> getOrderChangeLogs(
+            HttpServletRequest request,
+            @ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        try {
+            String authorization = getAuthorization(request);
+            log.info("查询订单变更记录: orderId={}", orderId);
+            return diningServiceFeign.getOrderChangeLogs(authorization, orderId);
+        } catch (Exception e) {
+            log.error("查询订单变更记录失败: {}", e.getMessage(), e);
+            return R.fail("查询订单变更记录失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 用户相关接口 ====================
+
+    @ApiOperation(value = "获取用户信息", notes = "获取当前登录用户的详细信息(从token中获取用户ID)")
+    @ApiOperationSupport(order = 9)
+    @GetMapping("/user/info")
+    public R<Object> getUserInfo(HttpServletRequest request) {
+        try {
+            String authorization = getAuthorization(request);
+            log.info("获取用户信息");
+            return diningServiceFeign.getUserInfo(authorization);
+        } catch (Exception e) {
+            log.error("获取用户信息失败: {}", e.getMessage(), e);
+            return R.fail("获取用户信息失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 门店信息接口 ====================
+
+    @ApiOperation(value = "根据门店ID查询桌号列表", notes = "查询指定门店下的所有桌号")
+    @ApiOperationSupport(order = 10)
+    @GetMapping("/store/tables")
+    public R<List<StoreTable>> getTablesByStoreId(
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId) {
+        try {
+            log.info("根据门店ID查询桌号列表: storeId={}", storeId);
+            return diningServiceFeign.getTablesByStoreId(storeId);
+        } catch (Exception e) {
+            log.error("根据门店ID查询桌号列表失败: {}", e.getMessage(), e);
+            return R.fail("根据门店ID查询桌号列表失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 文件上传接口 ====================
+
+    @ApiOperation(value = "上传图片到OSS", notes = "支持图片、视频或PDF文件上传,返回OSS文件路径")
+    @ApiOperationSupport(order = 11)
+    @PostMapping(value = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public R<String> uploadFile(
+            @ApiParam(value = "文件对象", required = true) @RequestParam("file") MultipartFile file) {
+        try {
+            if (file == null || file.isEmpty()) {
+                return R.fail("文件不能为空");
+            }
+            log.info("上传文件: fileName={}, size={}", file.getOriginalFilename(), file.getSize());
+            return diningServiceFeign.uploadFile(file);
+        } catch (Exception e) {
+            log.error("上传文件失败: {}", e.getMessage(), e);
+            return R.fail("上传文件失败: " + e.getMessage());
+        }
+    }
+}

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

@@ -0,0 +1,218 @@
+package shop.alien.store.feign;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreTable;
+import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.entity.store.vo.*;
+
+import java.util.List;
+
+/**
+ * 点餐服务 Feign 客户端
+ * 用于 alien-store 模块调用 alien-dining 模块的接口
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/XX
+ */
+@FeignClient(name = "alien-dining", url = "${feign.alienDining.url:}")
+public interface DiningServiceFeign {
+
+    // ==================== 点餐相关接口 ====================
+
+    /**
+     * 获取点餐页面信息
+     *
+     * @param authorization 请求头 Authorization,供 dining 解析当前用户
+     * @param tableId       桌号ID
+     * @param dinerCount    就餐人数
+     * @return R.data 为 DiningPageInfoVO
+     */
+    @GetMapping("/store/dining/page-info")
+    R<DiningPageInfoVO> getDiningPageInfo(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("tableId") Integer tableId,
+            @RequestParam("dinerCount") Integer dinerCount);
+
+    /**
+     * 搜索菜品
+     *
+     * @param authorization 请求头 Authorization
+     * @param storeId       门店ID
+     * @param keyword       搜索关键词
+     * @param tableId       桌号ID
+     * @return R.data 为 List&lt;CuisineListVO&gt;
+     */
+    @GetMapping("/store/dining/search")
+    R<List<CuisineListVO>> searchCuisines(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("storeId") Integer storeId,
+            @RequestParam("keyword") String keyword,
+            @RequestParam("tableId") Integer tableId);
+
+    /**
+     * 根据分类获取菜品列表
+     *
+     * @param authorization 请求头 Authorization
+     * @param storeId       门店ID
+     * @param categoryId    分类ID(可为空,查询所有)
+     * @param tableId       桌号ID
+     * @param page          页码(默认1)
+     * @param size          每页数量(默认12)
+     * @return R.data 为 List&lt;CuisineListVO&gt;
+     */
+    @GetMapping("/store/dining/cuisines")
+    R<List<CuisineListVO>> getCuisinesByCategory(
+            @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);
+
+    /**
+     * 获取菜品详情
+     *
+     * @param authorization 请求头 Authorization
+     * @param cuisineId     菜品ID
+     * @param tableId       桌号ID
+     * @return R.data 为 CuisineDetailVO
+     */
+    @GetMapping("/store/dining/cuisine/{cuisineId}")
+    R<CuisineDetailVO> getCuisineDetail(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @PathVariable("cuisineId") Integer cuisineId,
+            @RequestParam("tableId") Integer tableId);
+
+    /**
+     * 获取可领取的优惠券列表
+     *
+     * @param authorization 请求头 Authorization
+     * @param storeId       门店ID
+     * @return R.data 为 List&lt;AvailableCouponVO&gt;
+     */
+    @GetMapping("/store/dining/coupons/available")
+    R<List<AvailableCouponVO>> getAvailableCoupons(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("storeId") Integer storeId);
+
+    // ==================== 订单相关接口 ====================
+
+    /**
+     * 获取购物车
+     *
+     * @param authorization 请求头 Authorization
+     * @param tableId       桌号ID
+     * @return R.data 为 CartDTO
+     */
+    @GetMapping("/store/order/cart/{tableId}")
+    R<CartDTO> getCart(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @PathVariable("tableId") Integer tableId);
+
+    /**
+     * 获取订单详情
+     *
+     * @param authorization 请求头 Authorization
+     * @param orderId       订单ID
+     * @return R.data 为 OrderDetailWithChangeLogVO
+     */
+    @GetMapping("/store/order/detail/{orderId}")
+    R<OrderDetailWithChangeLogVO> getOrderDetail(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @PathVariable("orderId") Integer orderId);
+
+    /**
+     * 获取订单列表(分页)
+     *
+     * @param authorization 请求头 Authorization
+     * @param page          页码
+     * @param size          每页数量
+     * @param status        订单状态(可选)
+     * @return R.data 为 IPage&lt;StoreOrderPageVO&gt;
+     */
+    @GetMapping("/store/order/list")
+    R<IPage<StoreOrderPageVO>> getOrderList(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam(value = "page", defaultValue = "1") Integer page,
+            @RequestParam(value = "size", defaultValue = "10") Integer size,
+            @RequestParam(value = "status", required = false) Integer status);
+
+    /**
+     * 分页查询订单列表
+     * 包含订单中的菜品数量、菜品名称、菜品图片。支持按订单编号或菜品名称搜索(限15字)
+     *
+     * @param authorization 请求头 Authorization,供 dining 解析当前用户
+     * @param current       页码(默认1)
+     * @param size         每页数量(默认10)
+     * @param storeId       门店ID(可选)
+     * @param tableId       桌号ID(可选)
+     * @param orderStatus   订单状态(可选,0:待支付, 1:已支付, 2:已取消, 3:已完成)
+     * @param keyword       搜索关键词(订单编号或菜品名称,限15字)(可选)
+     * @return R.data 为 Page&lt;StoreOrderPageVO&gt;(使用具体的 Page 实现类,避免 IPage 反序列化问题)
+     */
+    @GetMapping("/store/order/page")
+    R<Page<StoreOrderPageVO>> getOrderPage(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam(value = "current", defaultValue = "1") Long current,
+            @RequestParam(value = "size", defaultValue = "10") Long size,
+            @RequestParam(value = "storeId", required = false) Integer storeId,
+            @RequestParam(value = "tableId", required = false) Integer tableId,
+            @RequestParam(value = "orderStatus", required = false) Integer orderStatus,
+            @RequestParam(value = "keyword", required = false) String keyword);
+
+    /**
+     * 查询订单变更记录
+     *
+     * @param authorization 请求头 Authorization
+     * @param orderId       订单ID
+     * @return R.data 为 List&lt;OrderChangeLogBatchVO&gt;
+     */
+    @GetMapping("/store/order/change-log/{orderId}")
+    R<List<OrderChangeLogBatchVO>> getOrderChangeLogs(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @PathVariable("orderId") Integer orderId);
+
+    // ==================== 用户相关接口 ====================
+
+    /**
+     * 获取用户信息
+     * 注意:返回类型使用 Object,因为 DiningUserVo 在 alien-dining 模块中
+     * 调用方需要自行转换为对应的 VO 对象
+     *
+     * @param authorization 请求头 Authorization
+     * @return R.data 为用户信息对象
+     */
+    @GetMapping("/dining/user/getUserInfo")
+    R<Object> getUserInfo(
+            @RequestHeader(value = "Authorization", required = false) String authorization);
+
+    // ==================== 门店信息接口 ====================
+
+    /**
+     * 根据门店ID查询桌号列表
+     *
+     * @param storeId 门店ID
+     * @return R.data 为 List&lt;StoreTable&gt;
+     */
+    @GetMapping("/store/info/tables")
+    R<List<StoreTable>> getTablesByStoreId(
+            @RequestParam("storeId") Integer storeId);
+
+    // ==================== 文件上传接口 ====================
+
+    /**
+     * 上传图片到OSS(单个文件上传,支持图片、视频或PDF)
+     *
+     * @param file 文件对象
+     * @return R.data 为文件路径(String)
+     */
+    @PostMapping(value = "/dining/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    R<String> uploadFile(@RequestPart("file") MultipartFile file);
+}

+ 10 - 4
alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponServiceImpl.java

@@ -129,8 +129,11 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
             //发布优惠券表信息
             LifeDiscountCoupon lifeDiscountCoupon = new LifeDiscountCoupon();
             BeanUtils.copyProperties(lifeDiscountCouponDto, lifeDiscountCoupon);
-            // 如果最低消费为null,设置为0(表示无门槛)
-            if (lifeDiscountCoupon.getMinimumSpendingAmount() == null) {
+            // 显式设置最低消费金额(门槛费),确保能正确保存
+            // 如果 DTO 中有值,使用 DTO 的值;如果为 null,设置为 0(表示无门槛)
+            if (lifeDiscountCouponDto.getMinimumSpendingAmount() != null) {
+                lifeDiscountCoupon.setMinimumSpendingAmount(lifeDiscountCouponDto.getMinimumSpendingAmount());
+            } else {
                 lifeDiscountCoupon.setMinimumSpendingAmount(BigDecimal.ZERO);
             }
             // 根据开始领取时间判断可领取状态
@@ -224,8 +227,11 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
             LifeDiscountCoupon lifeDiscountCoupon = new LifeDiscountCoupon();
             lifeDiscountCoupon.setId(Integer.parseInt(lifeDiscountCouponDto.getCouponId()));
             BeanUtils.copyProperties(lifeDiscountCouponDto, lifeDiscountCoupon);
-            // 如果最低消费为null,设置为0(表示无门槛)
-            if (lifeDiscountCoupon.getMinimumSpendingAmount() == null) {
+            // 显式设置最低消费金额(门槛费),确保能正确保存
+            // 如果 DTO 中有值,使用 DTO 的值;如果为 null,设置为 0(表示无门槛)
+            if (lifeDiscountCouponDto.getMinimumSpendingAmount() != null) {
+                lifeDiscountCoupon.setMinimumSpendingAmount(lifeDiscountCouponDto.getMinimumSpendingAmount());
+            } else {
                 lifeDiscountCoupon.setMinimumSpendingAmount(BigDecimal.ZERO);
             }
 

+ 6 - 0
订单系统表结构说明.md

@@ -262,6 +262,12 @@ CREATE TABLE `store_order_detail` (
 - `0`: 初始下单
 - `1`: 加餐
 
+### 餐桌状态(status)
+- `0`: 空闲
+- `1`: 就餐中
+- `2`: 其他
+- `3`: 加餐(当首次订单发生变化时,状态从"就餐中"变为"加餐")
+
 ---
 
 ## 五、索引说明