ソースを参照

开发订单变更记录功能

lutong 2 ヶ月 前
コミット
60ff8a3a72

+ 152 - 0
alien-dining/doc/订单变更记录表使用说明.md

@@ -0,0 +1,152 @@
+# 订单变更记录表(store_order_change_log)使用说明
+
+## 表设计目的
+
+记录每次下单/加餐时商品种类和数量的变化,用于展示每次操作都加了什么商品。
+
+## 核心字段说明
+
+### 1. batch_no(批次号)
+- **作用**:同一时间点的操作使用同一批次号,用于分组展示
+- **格式建议**:`ORDER{orderId}_{timestamp}` 或 `ORDER{orderId}_{yyyyMMddHHmmss}`
+- **示例**:`ORDER123_20250202143025` 表示订单123在2025-02-02 14:30:25的操作批次
+
+### 2. operation_type(操作类型)
+- **1:首次下单** - 创建订单时的操作
+- **2:加餐** - 通过加餐接口添加商品
+- **3:更新订单** - 更新订单时重新下单
+
+### 3. quantity_change(数量变化)
+- **正数**:新增的数量
+- **负数**:减少的数量(如果支持减少)
+- **示例**:`+2` 表示新增2份,`-1` 表示减少1份
+
+### 4. quantity_before / quantity_after(变化前后数量)
+- **quantity_before**:变化前的数量
+- **quantity_after**:变化后的数量
+- **用途**:用于展示变化前后对比
+
+## 使用场景
+
+### 场景1:查询订单的所有变更记录(按时间排序)
+```sql
+SELECT 
+    batch_no,
+    operation_type,
+    operation_time,
+    cuisine_name,
+    quantity_change,
+    quantity_before,
+    quantity_after,
+    amount_change
+FROM store_order_change_log 
+WHERE order_id = 123 
+  AND delete_flag = 0
+ORDER BY operation_time ASC;
+```
+
+### 场景2:按批次分组展示每次操作
+```sql
+SELECT 
+    batch_no,
+    operation_type,
+    operation_time,
+    COUNT(*) as item_count,
+    SUM(quantity_change) as total_quantity_change,
+    SUM(amount_change) as total_amount_change,
+    GROUP_CONCAT(CONCAT(cuisine_name, ' x', quantity_change) SEPARATOR ', ') as items
+FROM store_order_change_log 
+WHERE order_id = 123 
+  AND delete_flag = 0
+GROUP BY batch_no, operation_type, operation_time
+ORDER BY operation_time ASC;
+```
+
+### 场景3:查询某次操作的所有商品
+```sql
+SELECT 
+    cuisine_name,
+    quantity_change,
+    unit_price,
+    amount_change
+FROM store_order_change_log 
+WHERE batch_no = 'ORDER123_20250202143025'
+  AND delete_flag = 0
+ORDER BY cuisine_id;
+```
+
+### 场景4:查询加餐记录
+```sql
+SELECT 
+    batch_no,
+    operation_time,
+    cuisine_name,
+    quantity_change,
+    amount_change
+FROM store_order_change_log 
+WHERE order_id = 123 
+  AND operation_type = 2  -- 加餐
+  AND delete_flag = 0
+ORDER BY operation_time DESC;
+```
+
+## 数据记录示例
+
+假设订单123的操作历史:
+
+### 首次下单(batch_no: ORDER123_20250202140000)
+| cuisine_name | quantity_change | quantity_before | quantity_after | amount_change |
+|-------------|----------------|-----------------|----------------|---------------|
+| 石板肉酱豆腐 | 1 | 0 | 1 | 19.90 |
+| 经典三杯鸡 | 1 | 0 | 1 | 26.90 |
+
+### 第一次加餐(batch_no: ORDER123_20250202143000)
+| cuisine_name | quantity_change | quantity_before | quantity_after | amount_change |
+|-------------|----------------|-----------------|----------------|---------------|
+| 经典三杯鸡 | 1 | 1 | 2 | 26.90 |
+| 宫保鸡丁 | 1 | 0 | 1 | 28.00 |
+
+### 第二次加餐(batch_no: ORDER123_20250202150000)
+| cuisine_name | quantity_change | quantity_before | quantity_after | amount_change |
+|-------------|----------------|-----------------|----------------|---------------|
+| 石板肉酱豆腐 | 1 | 1 | 2 | 19.90 |
+
+## 前端展示建议
+
+### 展示格式1:时间线展示
+```
+14:00 - 首次下单
+  • 石板肉酱豆腐 x1
+  • 经典三杯鸡 x1
+
+14:30 - 加餐
+  • 经典三杯鸡 x1(累计:2)
+  • 宫保鸡丁 x1
+
+15:00 - 加餐
+  • 石板肉酱豆腐 x1(累计:2)
+```
+
+### 展示格式2:批次卡片展示
+```
+批次1:首次下单(14:00)
+  石板肉酱豆腐 x1  ¥19.90
+  经典三杯鸡 x1    ¥26.90
+  小计:¥46.80
+
+批次2:加餐(14:30)
+  经典三杯鸡 x1    ¥26.90
+  宫保鸡丁 x1      ¥28.00
+  小计:¥54.90
+
+批次3:加餐(15:00)
+  石板肉酱豆腐 x1  ¥19.90
+  小计:¥19.90
+```
+
+## 注意事项
+
+1. **批次号生成**:同一时间点的所有商品变化应该使用同一个批次号
+2. **数量计算**:quantity_change 应该只记录本次变化,不是累计数量
+3. **逻辑删除**:使用 delete_flag 进行逻辑删除,保留历史记录
+4. **索引优化**:已创建必要的索引,查询性能有保障

+ 18 - 0
alien-dining/src/main/java/shop/alien/dining/controller/StoreOrderController.java

@@ -20,6 +20,7 @@ import shop.alien.entity.store.dto.AddCartItemDTO;
 import shop.alien.entity.store.dto.CartDTO;
 import shop.alien.entity.store.dto.ChangeTableDTO;
 import shop.alien.entity.store.dto.CreateOrderDTO;
+import shop.alien.entity.store.vo.OrderChangeLogBatchVO;
 import shop.alien.entity.store.vo.OrderInfoVO;
 import shop.alien.mapper.StoreInfoMapper;
 import shop.alien.mapper.StoreOrderDetailMapper;
@@ -395,6 +396,23 @@ public class StoreOrderController {
         }
     }
 
+    @ApiOperation(value = "查询订单变更记录", notes = "根据订单ID查询订单的所有变更记录(按批次分组),用于展示每次下单/加餐都加了什么商品")
+    @GetMapping("/change-log/{orderId}")
+    public R<List<OrderChangeLogBatchVO>> getOrderChangeLogs(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            List<OrderChangeLogBatchVO> changeLogs = orderService.getOrderChangeLogs(orderId);
+            return R.data(changeLogs);
+        } catch (Exception e) {
+            log.error("查询订单变更记录失败: {}", e.getMessage(), e);
+            return R.fail("查询订单变更记录失败: " + e.getMessage());
+        }
+    }
+
     @ApiOperation(value = "分页查询订单列表", notes = "分页查询订单列表")
     @GetMapping("/page")
     public R<IPage<StoreOrder>> getOrderPage(

+ 11 - 0
alien-dining/src/main/java/shop/alien/dining/service/StoreOrderService.java

@@ -5,8 +5,11 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
 import shop.alien.entity.store.StoreOrder;
 import shop.alien.entity.store.dto.CreateOrderDTO;
+import shop.alien.entity.store.vo.OrderChangeLogBatchVO;
 import shop.alien.entity.store.vo.OrderInfoVO;
 
+import java.util.List;
+
 /**
  * 订单服务接口
  *
@@ -110,4 +113,12 @@ public interface StoreOrderService extends IService<StoreOrder> {
      * @return 订单信息
      */
     OrderInfoVO getOrderInfo(Integer orderId);
+
+    /**
+     * 查询订单的所有变更记录(按批次分组)
+     *
+     * @param orderId 订单ID
+     * @return 变更记录批次列表
+     */
+    List<OrderChangeLogBatchVO> getOrderChangeLogs(Integer orderId);
 }

+ 248 - 42
alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java

@@ -18,13 +18,14 @@ import shop.alien.entity.store.*;
 import shop.alien.entity.store.dto.CartDTO;
 import shop.alien.entity.store.dto.CartItemDTO;
 import shop.alien.entity.store.dto.CreateOrderDTO;
+import shop.alien.entity.store.vo.OrderChangeLogBatchVO;
+import shop.alien.entity.store.vo.OrderChangeLogItemVO;
 import shop.alien.entity.store.vo.OrderInfoVO;
 import shop.alien.mapper.*;
 
 import java.math.BigDecimal;
 import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.List;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -50,6 +51,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
     private final StoreCartMapper storeCartMapper;
     private final BaseRedisService baseRedisService;
     private final StoreInfoMapper storeInfoMapper;
+    private final StoreOrderChangeLogMapper orderChangeLogMapper;
 
     @Override
     public StoreOrder createOrder(CreateOrderDTO dto) {
@@ -73,6 +75,31 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         if (cart.getItems() == null || cart.getItems().isEmpty()) {
             throw new RuntimeException("购物车为空");
         }
+        
+        // 检查是否有新增商品或商品数量增加
+        boolean hasNewItems = false; // 是否有新增商品(未下单过的商品)
+        boolean hasQuantityIncrease = false; // 是否有商品数量增加
+        
+        for (shop.alien.entity.store.dto.CartItemDTO item : cart.getItems()) {
+            Integer lockedQuantity = item.getLockedQuantity();
+            Integer currentQuantity = item.getQuantity();
+            
+            if (lockedQuantity == null || lockedQuantity == 0) {
+                // 有新增商品(未下单过的商品)
+                hasNewItems = true;
+                log.debug("发现新增商品, cuisineId={}, quantity={}", item.getCuisineId(), currentQuantity);
+            } else if (currentQuantity != null && currentQuantity > lockedQuantity) {
+                // 有商品数量增加(当前数量大于已下单数量)
+                hasQuantityIncrease = true;
+                log.debug("发现商品数量增加, cuisineId={}, currentQuantity={}, orderedQuantity={}", 
+                        item.getCuisineId(), currentQuantity, lockedQuantity);
+            }
+        }
+        
+        // 如果没有新增商品且没有商品数量增加,不允许创建订单
+        if (!hasNewItems && !hasQuantityIncrease) {
+            throw new RuntimeException("购物车中没有新增商品或商品数量未增加,无法创建订单");
+        }
 
         // 验证优惠券(可选,couponId 可以为 null,不选择优惠券时 discountAmount 为 0)
         BigDecimal discountAmount = BigDecimal.ZERO;
@@ -127,39 +154,96 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         // 计算实付金额(菜品总价 + 餐具费 - 优惠金额)
         BigDecimal payAmount = cart.getTotalAmount().add(tablewareFee).subtract(discountAmount);
 
-        // 生成订单号
-        String orderNo = generateOrderNo();
-
-        // 创建订单
         Date now = new Date();
-        StoreOrder order = new StoreOrder();
-        order.setOrderNo(orderNo);
-        order.setStoreId(table.getStoreId());
-        order.setTableId(table.getId());
-        order.setTableNumber(table.getTableNumber());
-        order.setDinerCount(dto.getDinerCount());
-        order.setContactPhone(dto.getContactPhone());
-        order.setTablewareFee(tablewareFee);
-        order.setPayUserId(userId);
-        order.setPayUserPhone(userPhone);
-        order.setOrderStatus(0); // 待支付
-        order.setTotalAmount(cart.getTotalAmount());
-        order.setCouponId(dto.getCouponId());
-        order.setCurrentCouponId(dto.getCouponId()); // 记录当前使用的优惠券
-        order.setDiscountAmount(discountAmount);
-        order.setPayAmount(payAmount);
+        StoreOrder order = null;
+        String orderNo = null;
+        boolean isUpdate = false; // 是否是更新订单
         
-        // 如果immediatePay为0,只创建订单不支付;为1则创建订单并支付
-        // 暂时不实现立即支付,由前端调用支付接口
-        // payType在支付时设置
-        order.setPayStatus(0); // 未支付
-        order.setRemark(dto.getRemark());
-        order.setCreatedUserId(userId);
-        order.setUpdatedUserId(userId);
-        // 手动设置创建时间和更新时间(临时方案,避免自动填充未生效导致 created_time 为 null)
-        order.setCreatedTime(now);
-        order.setUpdatedTime(now);
-        this.save(order);
+        // 检查桌号是否已绑定订单
+        if (table.getCurrentOrderId() != null) {
+            // 查询已存在的订单
+            StoreOrder existingOrder = this.getById(table.getCurrentOrderId());
+            if (existingOrder != null && existingOrder.getOrderStatus() == 0 && existingOrder.getPayStatus() == 0) {
+                // 订单存在且是待支付状态,更新订单
+                isUpdate = true;
+                order = existingOrder;
+                orderNo = order.getOrderNo(); // 使用原订单号
+                log.info("桌号已绑定订单,更新订单信息, orderId={}, orderNo={}", order.getId(), orderNo);
+                
+                // 更新订单信息
+                order.setDinerCount(dto.getDinerCount());
+                order.setContactPhone(dto.getContactPhone());
+                order.setTablewareFee(tablewareFee);
+                order.setTotalAmount(cart.getTotalAmount());
+                order.setCouponId(dto.getCouponId());
+                order.setCurrentCouponId(dto.getCouponId());
+                order.setDiscountAmount(discountAmount);
+                order.setPayAmount(payAmount);
+                order.setRemark(dto.getRemark());
+                order.setUpdatedUserId(userId);
+                order.setUpdatedTime(now);
+                this.updateById(order);
+                
+                // 逻辑删除旧的订单明细
+                LambdaQueryWrapper<StoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
+                detailWrapper.eq(StoreOrderDetail::getOrderId, order.getId());
+                detailWrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
+                List<StoreOrderDetail> oldDetails = orderDetailMapper.selectList(detailWrapper);
+                if (oldDetails != null && !oldDetails.isEmpty()) {
+                    List<Integer> oldDetailIds = oldDetails.stream()
+                            .map(StoreOrderDetail::getId)
+                            .collect(Collectors.toList());
+                    orderDetailMapper.deleteBatchIds(oldDetailIds);
+                    log.info("删除旧订单明细, orderId={}, count={}", order.getId(), oldDetailIds.size());
+                }
+            } else {
+                log.info("桌号绑定的订单不存在或已支付/已取消,将创建新订单");
+            }
+        }
+        
+        // 如果没有订单需要更新,创建新订单
+        if (!isUpdate) {
+            // 生成订单号
+            orderNo = generateOrderNo();
+            
+            // 创建订单
+            order = new StoreOrder();
+            order.setOrderNo(orderNo);
+            order.setStoreId(table.getStoreId());
+            order.setTableId(table.getId());
+            order.setTableNumber(table.getTableNumber());
+            order.setDinerCount(dto.getDinerCount());
+            order.setContactPhone(dto.getContactPhone());
+            order.setTablewareFee(tablewareFee);
+            order.setPayUserId(userId);
+            order.setPayUserPhone(userPhone);
+            order.setOrderStatus(0); // 待支付
+            order.setTotalAmount(cart.getTotalAmount());
+            order.setCouponId(dto.getCouponId());
+            order.setCurrentCouponId(dto.getCouponId()); // 记录当前使用的优惠券
+            order.setDiscountAmount(discountAmount);
+            order.setPayAmount(payAmount);
+            
+            // 如果immediatePay为0,只创建订单不支付;为1则创建订单并支付
+            // 暂时不实现立即支付,由前端调用支付接口
+            // payType在支付时设置
+            order.setPayStatus(0); // 未支付
+            order.setRemark(dto.getRemark());
+            order.setCreatedUserId(userId);
+            order.setUpdatedUserId(userId);
+            // 手动设置创建时间和更新时间(临时方案,避免自动填充未生效导致 created_time 为 null)
+            order.setCreatedTime(now);
+            order.setUpdatedTime(now);
+            this.save(order);
+            log.info("创建新订单, orderId={}, orderNo={}", order.getId(), orderNo);
+        }
+        
+        // 确保 order 和 orderNo 已初始化(用于后续代码)
+        if (order == null || orderNo == null) {
+            throw new RuntimeException("订单创建失败,系统错误");
+        }
+        final StoreOrder finalOrder = order;
+        final String finalOrderNo = orderNo;
 
         // 更新优惠券使用记录状态为已下单
         if (dto.getCouponId() != null) {
@@ -171,19 +255,22 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
             usageWrapper.last("LIMIT 1");
             StoreCouponUsage usage = storeCouponUsageMapper.selectOne(usageWrapper);
             if (usage != null) {
-                usage.setOrderId(order.getId());
+                usage.setOrderId(finalOrder.getId());
                 usage.setUsageStatus(1); // 已下单
                 usage.setUpdatedTime(new Date());
                 storeCouponUsageMapper.updateById(usage);
             }
+        } else if (isUpdate) {
+            // 如果是更新订单且没有使用优惠券,需要清除之前的优惠券使用记录
+            // 这里暂时不处理,因为可能用户只是想更新订单信息,不想改变优惠券
         }
 
         // 创建订单明细
         List<StoreOrderDetail> orderDetails = cart.getItems().stream()
                 .map(item -> {
                     StoreOrderDetail detail = new StoreOrderDetail();
-                    detail.setOrderId(order.getId());
-                    detail.setOrderNo(orderNo);
+                    detail.setOrderId(finalOrder.getId());
+                    detail.setOrderNo(finalOrderNo);
                     detail.setCuisineId(item.getCuisineId());
                     detail.setCuisineName(item.getCuisineName());
                     detail.setCuisineType(item.getCuisineType());
@@ -205,10 +292,12 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
             orderDetailMapper.insert(detail);
         }
 
-        // 更新桌号的当前订单ID
-        table.setCurrentOrderId(order.getId());
-        table.setStatus(1); // 就餐中
-        storeTableMapper.updateById(table);
+        // 更新桌号的当前订单ID(如果是新订单才需要更新,更新订单时已经绑定了)
+        if (!isUpdate) {
+            table.setCurrentOrderId(finalOrder.getId());
+            table.setStatus(1); // 就餐中
+            storeTableMapper.updateById(table);
+        }
 
         // 锁定购物车商品数量(禁止减少或删除已下单的商品)
         cartService.lockCartItems(dto.getTableId());
@@ -216,8 +305,12 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         // 下单后不清空购物车,允许加餐(加餐时添加到同一订单)
         // 只有在支付完成后才清空购物车
 
-        log.info("订单创建成功, orderId={}, orderNo={}", order.getId(), orderNo);
-        return order;
+        if (isUpdate) {
+            log.info("订单更新成功, orderId={}, orderNo={}", finalOrder.getId(), finalOrderNo);
+        } else {
+            log.info("订单创建成功, orderId={}, orderNo={}", finalOrder.getId(), finalOrderNo);
+        }
+        return finalOrder;
     }
 
     @Override
@@ -638,6 +731,22 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
                 }
             }
             
+            // 删除订单变更记录(逻辑删除,使用 MyBatis-Plus 的 deleteBatchIds)
+            for (Integer orderId : orderIds) {
+                LambdaQueryWrapper<StoreOrderChangeLog> changeLogWrapper = new LambdaQueryWrapper<>();
+                changeLogWrapper.eq(StoreOrderChangeLog::getOrderId, orderId);
+                changeLogWrapper.eq(StoreOrderChangeLog::getDeleteFlag, 0);
+                List<StoreOrderChangeLog> changeLogList = orderChangeLogMapper.selectList(changeLogWrapper);
+                if (changeLogList != null && !changeLogList.isEmpty()) {
+                    List<Integer> changeLogIds = changeLogList.stream()
+                            .map(StoreOrderChangeLog::getId)
+                            .collect(Collectors.toList());
+                    // 使用 deleteBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
+                    orderChangeLogMapper.deleteBatchIds(changeLogIds);
+                    log.info("删除订单变更记录, orderId={}, count={}", orderId, changeLogList.size());
+                }
+            }
+            
             // 删除订单(逻辑删除,使用 MyBatis-Plus 的 removeByIds)
             // 使用 removeByIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
             this.removeByIds(orderIds);
@@ -744,6 +853,103 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         return vo;
     }
 
+    @Override
+    public List<OrderChangeLogBatchVO> getOrderChangeLogs(Integer orderId) {
+        log.info("查询订单变更记录, orderId={}", orderId);
+        
+        // 1. 查询订单的所有变更记录
+        LambdaQueryWrapper<StoreOrderChangeLog> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreOrderChangeLog::getOrderId, orderId);
+        wrapper.eq(StoreOrderChangeLog::getDeleteFlag, 0);
+        wrapper.orderByAsc(StoreOrderChangeLog::getOperationTime);
+        wrapper.orderByAsc(StoreOrderChangeLog::getBatchNo);
+        List<StoreOrderChangeLog> logs = orderChangeLogMapper.selectList(wrapper);
+        
+        if (logs == null || logs.isEmpty()) {
+            log.info("订单没有变更记录, orderId={}", orderId);
+            return new ArrayList<>();
+        }
+        
+        // 2. 按批次号分组
+        Map<String, List<StoreOrderChangeLog>> batchMap = logs.stream()
+                .collect(Collectors.groupingBy(StoreOrderChangeLog::getBatchNo));
+        
+        // 3. 转换为批次VO列表
+        List<OrderChangeLogBatchVO> batchList = new ArrayList<>();
+        for (Map.Entry<String, List<StoreOrderChangeLog>> entry : batchMap.entrySet()) {
+            String batchNo = entry.getKey();
+            List<StoreOrderChangeLog> batchLogs = entry.getValue();
+            
+            if (batchLogs.isEmpty()) {
+                continue;
+            }
+            
+            // 取第一条记录作为批次信息(同一批次的操作类型、时间等信息相同)
+            StoreOrderChangeLog firstLog = batchLogs.get(0);
+            
+            OrderChangeLogBatchVO batchVO = new OrderChangeLogBatchVO();
+            batchVO.setBatchNo(batchNo);
+            batchVO.setOperationType(firstLog.getOperationType());
+            batchVO.setOperationTypeText(getOperationTypeText(firstLog.getOperationType()));
+            batchVO.setOperationTime(firstLog.getOperationTime());
+            batchVO.setOperatorUserId(firstLog.getOperatorUserId());
+            batchVO.setOperatorUserPhone(firstLog.getOperatorUserPhone());
+            
+            // 计算批次统计信息
+            Integer totalQuantityChange = batchLogs.stream()
+                    .mapToInt(log -> log.getQuantityChange() != null ? log.getQuantityChange() : 0)
+                    .sum();
+            BigDecimal totalAmountChange = batchLogs.stream()
+                    .map(log -> log.getAmountChange() != null ? log.getAmountChange() : BigDecimal.ZERO)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            
+            batchVO.setTotalQuantityChange(totalQuantityChange);
+            batchVO.setTotalAmountChange(totalAmountChange);
+            batchVO.setItemCount(batchLogs.size());
+            
+            // 转换为商品项VO列表
+            List<OrderChangeLogItemVO> items = batchLogs.stream().map(log -> {
+                OrderChangeLogItemVO itemVO = new OrderChangeLogItemVO();
+                itemVO.setCuisineId(log.getCuisineId());
+                itemVO.setCuisineName(log.getCuisineName());
+                itemVO.setCuisineType(log.getCuisineType());
+                itemVO.setCuisineImage(log.getCuisineImage());
+                itemVO.setUnitPrice(log.getUnitPrice());
+                itemVO.setQuantityChange(log.getQuantityChange());
+                itemVO.setQuantityBefore(log.getQuantityBefore());
+                itemVO.setQuantityAfter(log.getQuantityAfter());
+                itemVO.setAmountChange(log.getAmountChange());
+                itemVO.setRemark(log.getRemark());
+                return itemVO;
+            }).collect(Collectors.toList());
+            
+            batchVO.setItems(items);
+            batchList.add(batchVO);
+        }
+        
+        log.info("查询订单变更记录完成, orderId={}, batchCount={}", orderId, batchList.size());
+        return batchList;
+    }
+    
+    /**
+     * 获取操作类型文本
+     */
+    private String getOperationTypeText(Integer operationType) {
+        if (operationType == null) {
+            return "未知";
+        }
+        switch (operationType) {
+            case 1:
+                return "首次下单";
+            case 2:
+                return "加餐";
+            case 3:
+                return "更新订单";
+            default:
+                return "未知";
+        }
+    }
+
     /**
      * 生成订单号
      */

+ 120 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreOrderChangeLog.java

@@ -0,0 +1,120 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 订单变更记录表(记录每次下单/加餐的商品变化)
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+@Data
+@JsonInclude
+@TableName("store_order_change_log")
+@ApiModel(value = "StoreOrderChangeLog对象", description = "订单变更记录表")
+public class StoreOrderChangeLog {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "订单ID")
+    @TableField("order_id")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "订单号")
+    @TableField("order_no")
+    private String orderNo;
+
+    @ApiModelProperty(value = "批次号(同一时间点的操作使用同一批次号,用于分组展示)")
+    @TableField("batch_no")
+    private String batchNo;
+
+    @ApiModelProperty(value = "操作类型(1:首次下单, 2:加餐, 3:更新订单)")
+    @TableField("operation_type")
+    private Integer operationType;
+
+    @ApiModelProperty(value = "菜品ID")
+    @TableField("cuisine_id")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "菜品名称")
+    @TableField("cuisine_name")
+    private String cuisineName;
+
+    @ApiModelProperty(value = "菜品类型(1:单品, 2:套餐)")
+    @TableField("cuisine_type")
+    private Integer cuisineType;
+
+    @ApiModelProperty(value = "菜品图片")
+    @TableField("cuisine_image")
+    private String cuisineImage;
+
+    @ApiModelProperty(value = "单价")
+    @TableField("unit_price")
+    private BigDecimal unitPrice;
+
+    @ApiModelProperty(value = "数量变化(新增的数量,正数表示增加,负数表示减少)")
+    @TableField("quantity_change")
+    private Integer quantityChange;
+
+    @ApiModelProperty(value = "变化前数量(用于展示变化前后对比)")
+    @TableField("quantity_before")
+    private Integer quantityBefore;
+
+    @ApiModelProperty(value = "变化后数量(用于展示变化前后对比)")
+    @TableField("quantity_after")
+    private Integer quantityAfter;
+
+    @ApiModelProperty(value = "金额变化(新增的金额)")
+    @TableField("amount_change")
+    private BigDecimal amountChange;
+
+    @ApiModelProperty(value = "操作时间")
+    @TableField("operation_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date operationTime;
+
+    @ApiModelProperty(value = "操作人ID")
+    @TableField("operator_user_id")
+    private Integer operatorUserId;
+
+    @ApiModelProperty(value = "操作人手机号")
+    @TableField("operator_user_phone")
+    private String operatorUserPhone;
+
+    @ApiModelProperty(value = "备注")
+    @TableField("remark")
+    private String remark;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 50 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/OrderChangeLogBatchVO.java

@@ -0,0 +1,50 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 订单变更记录批次VO(按批次分组)
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+@Data
+@ApiModel(value = "OrderChangeLogBatchVO对象", description = "订单变更记录批次")
+public class OrderChangeLogBatchVO {
+
+    @ApiModelProperty(value = "批次号")
+    private String batchNo;
+
+    @ApiModelProperty(value = "操作类型(1:首次下单, 2:加餐, 3:更新订单)")
+    private Integer operationType;
+
+    @ApiModelProperty(value = "操作类型文本")
+    private String operationTypeText;
+
+    @ApiModelProperty(value = "操作时间")
+    private Date operationTime;
+
+    @ApiModelProperty(value = "操作人ID")
+    private Integer operatorUserId;
+
+    @ApiModelProperty(value = "操作人手机号")
+    private String operatorUserPhone;
+
+    @ApiModelProperty(value = "该批次商品数量变化总和")
+    private Integer totalQuantityChange;
+
+    @ApiModelProperty(value = "该批次金额变化总和")
+    private BigDecimal totalAmountChange;
+
+    @ApiModelProperty(value = "该批次商品数量")
+    private Integer itemCount;
+
+    @ApiModelProperty(value = "该批次的商品明细列表")
+    private List<OrderChangeLogItemVO> items;
+}

+ 48 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/OrderChangeLogItemVO.java

@@ -0,0 +1,48 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 订单变更记录商品项VO
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+@Data
+@ApiModel(value = "OrderChangeLogItemVO对象", description = "订单变更记录商品项")
+public class OrderChangeLogItemVO {
+
+    @ApiModelProperty(value = "菜品ID")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "菜品名称")
+    private String cuisineName;
+
+    @ApiModelProperty(value = "菜品类型(1:单品, 2:套餐)")
+    private Integer cuisineType;
+
+    @ApiModelProperty(value = "菜品图片")
+    private String cuisineImage;
+
+    @ApiModelProperty(value = "单价")
+    private BigDecimal unitPrice;
+
+    @ApiModelProperty(value = "数量变化(新增的数量,正数表示增加,负数表示减少)")
+    private Integer quantityChange;
+
+    @ApiModelProperty(value = "变化前数量")
+    private Integer quantityBefore;
+
+    @ApiModelProperty(value = "变化后数量")
+    private Integer quantityAfter;
+
+    @ApiModelProperty(value = "金额变化")
+    private BigDecimal amountChange;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+}

+ 13 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreOrderChangeLogMapper.java

@@ -0,0 +1,13 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.StoreOrderChangeLog;
+
+/**
+ * 订单变更记录表 Mapper 接口
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+public interface StoreOrderChangeLogMapper extends BaseMapper<StoreOrderChangeLog> {
+}

+ 69 - 0
store_order_change_log.sql

@@ -0,0 +1,69 @@
+-- =============================================
+-- 订单变更记录表(记录每次下单/加餐的商品变化)
+-- 用于展示每次操作都加了什么商品
+-- =============================================
+
+CREATE TABLE IF NOT EXISTS `store_order_change_log` (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `order_id` int(11) NOT NULL COMMENT '订单ID',
+  `order_no` varchar(64) NOT NULL COMMENT '订单号',
+  `batch_no` varchar(64) NOT NULL COMMENT '批次号(同一时间点的操作使用同一批次号,用于分组展示)',
+  `operation_type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '操作类型(1:首次下单, 2:加餐, 3:更新订单)',
+  `cuisine_id` int(11) NOT NULL COMMENT '菜品ID',
+  `cuisine_name` varchar(200) NOT NULL COMMENT '菜品名称',
+  `cuisine_type` tinyint(4) DEFAULT NULL COMMENT '菜品类型(1:单品, 2:套餐)',
+  `cuisine_image` varchar(500) DEFAULT NULL COMMENT '菜品图片',
+  `unit_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '单价',
+  `quantity_change` int(11) NOT NULL DEFAULT '0' COMMENT '数量变化(新增的数量,正数表示增加,负数表示减少)',
+  `quantity_before` int(11) DEFAULT NULL COMMENT '变化前数量(用于展示变化前后对比)',
+  `quantity_after` int(11) DEFAULT NULL COMMENT '变化后数量(用于展示变化前后对比)',
+  `amount_change` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '金额变化(新增的金额)',
+  `operation_time` datetime NOT NULL COMMENT '操作时间',
+  `operator_user_id` int(11) DEFAULT NULL COMMENT '操作人ID',
+  `operator_user_phone` varchar(20) DEFAULT NULL COMMENT '操作人手机号',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  `delete_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记, 0:未删除, 1:已删除',
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `created_user_id` int(11) DEFAULT NULL COMMENT '创建人ID',
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+  `updated_user_id` int(11) DEFAULT NULL COMMENT '修改人ID',
+  PRIMARY KEY (`id`),
+  KEY `idx_order_id` (`order_id`),
+  KEY `idx_order_no` (`order_no`),
+  KEY `idx_batch_no` (`batch_no`),
+  KEY `idx_operation_type` (`operation_type`),
+  KEY `idx_operation_time` (`operation_time`),
+  KEY `idx_cuisine_id` (`cuisine_id`),
+  KEY `idx_operator_user_id` (`operator_user_id`),
+  KEY `idx_order_batch` (`order_id`, `batch_no`),
+  KEY `idx_order_time` (`order_id`, `operation_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单变更记录表(记录每次下单/加餐的商品变化)';
+
+-- =============================================
+-- 表结构说明:
+-- 
+-- 1. batch_no(批次号):
+--    - 同一时间点的操作使用同一批次号
+--    - 格式建议:ORDER{orderId}_{timestamp} 或 ORDER{orderId}_{yyyyMMddHHmmss}
+--    - 用于按批次分组,展示每次操作都加了什么商品
+-- 
+-- 2. operation_type(操作类型):
+--    - 1: 首次下单(创建订单时)
+--    - 2: 加餐(通过加餐接口添加)
+--    - 3: 更新订单(更新订单时重新下单)
+-- 
+-- 3. quantity_change(数量变化):
+--    - 正数:新增的数量
+--    - 负数:减少的数量(如果支持减少)
+--    - 0:数量未变化(理论上不应该出现)
+-- 
+-- 4. quantity_before / quantity_after(变化前后数量):
+--    - 用于展示变化前后对比
+--    - 首次下单时,quantity_before = 0, quantity_after = quantity_change
+--    - 加餐时,quantity_before = 原数量, quantity_after = 原数量 + quantity_change
+-- 
+-- 5. 使用场景:
+--    - 查询某个订单的所有变更记录:SELECT * FROM store_order_change_log WHERE order_id = ? ORDER BY operation_time
+--    - 按批次分组展示:SELECT batch_no, operation_type, operation_time, GROUP_CONCAT(...) FROM store_order_change_log WHERE order_id = ? GROUP BY batch_no ORDER BY operation_time
+--    - 查询某次操作的所有商品:SELECT * FROM store_order_change_log WHERE batch_no = ?
+-- =============================================