Просмотр исходного кода

订单状态使用新的方式

qmj 1 месяц назад
Родитель
Сommit
a70e786e3b

+ 1 - 1
CLAUDE.md

@@ -1,6 +1,6 @@
 # foodie_server Development Guidelines
 
-Auto-generated from all feature plans. Last updated: 2026-04-29
+Auto-generated from all feature plans. Last updated: 2026-05-15
 
 ## Tech Stack
 

+ 2 - 10
ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderAppealController.java

@@ -117,13 +117,9 @@ public class OrderAppealController extends BaseController {
         }
         PosOrder update = new PosOrder();
         update.setId(order.getId());
+        // 取消订单:state=4(已取消)
+        update.setState(4L);
         if ("1".equals(order.getCollectPayment())) {
-            //货到付款骑手未接单,直接作废
-            update.setState(10L);
-//            //退回积分
-//            if (order.getPoints() != null && order.getPoints() > 0) {
-//                walletService.returnPoints(order.getUserId(), order.getDdId(), Long.valueOf(order.getPoints()));
-//            }
             UserBilling bill = billingService.getOne(new LambdaQueryWrapper<UserBilling>().eq(UserBilling::getDdId, String.valueOf(order.getDdId())).eq(UserBilling::getUserId, Long.valueOf(userid)));
             //账单作废
             if (bill != null) {
@@ -133,10 +129,6 @@ public class OrderAppealController extends BaseController {
                 billingService.saveOrUpdate(user);
             }
         }
-//        else {
-            //在线支付客服接入
-//            update.setState(9L);
-//        }
         posOrderService.saveOrUpdate(update);
 //        PosAppeal appeal = new PosAppeal();
 //        appeal.setUserId(order.getUserId());

+ 101 - 25
ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderQsOprateController.java

@@ -53,10 +53,7 @@ public class PosOrderQsOprateController extends BaseController {
     private IUserBillingService userBillingService;
 
     /**
-     * 骑手接单
-     * @param token
-     * @param id
-     * @return
+     * 骑手接单:校验 type=0 且 afterSaleStatus=0,设置 deliveryStatus=1
      */
     @Anonymous
     @Auth
@@ -71,18 +68,21 @@ public class PosOrderQsOprateController extends BaseController {
         if (order == null) {
             throw new ServiceException(MessageUtils.message("no.order.not.found"));
         }
+        if (order.getType() != null && order.getType() != 0L) {
+            throw new ServiceException("仅外送订单支持骑手接单");
+        }
+        if (order.getAfterSaleStatus() != null && order.getAfterSaleStatus() > 0) {
+            throw new ServiceException("订单存在售后申请,无法操作");
+        }
         PosOrder posOrder = new PosOrder();
         posOrder.setId(order.getId());
-        posOrder.setState(3L);
+        posOrder.setDeliveryStatus(1L);
         posOrder.setQsId(Long.valueOf(qsId));
         return setOrderQsState(posOrder, qsId, push);
     }
 
     /**
-     * 骑手取餐
-     * @param token
-     * @param id
-     * @return
+     * 骑手取餐:校验 type=0 且 afterSaleStatus=0,设置 deliveryStatus=2
      */
     @Anonymous
     @Auth
@@ -97,17 +97,17 @@ public class PosOrderQsOprateController extends BaseController {
         if (order == null) {
             throw new ServiceException(MessageUtils.message("no.order.not.found"));
         }
+        if (order.getAfterSaleStatus() != null && order.getAfterSaleStatus() > 0) {
+            throw new ServiceException("订单存在售后申请,无法操作");
+        }
         PosOrder posOrder = new PosOrder();
         posOrder.setId(order.getId());
-        posOrder.setState(4L);
+        posOrder.setDeliveryStatus(2L);
         return setOrderQsState(posOrder, qsId, push);
     }
 
     /**
-     * 骑手送达
-     * @param token
-     * @param id
-     * @return
+     * 骑手送达:设置 deliveryStatus=3,同时设置 state=3
      */
     @Anonymous
     @Auth
@@ -124,7 +124,8 @@ public class PosOrderQsOprateController extends BaseController {
         }
         PosOrder posOrder = new PosOrder();
         posOrder.setId(order.getId());
-        posOrder.setState(12L);
+        posOrder.setDeliveryStatus(3L);
+        posOrder.setState(3L);
         posOrder.setSdTime(new Date());
         return setOrderQsState(posOrder, qsId, push);
     }
@@ -143,29 +144,29 @@ public class PosOrderQsOprateController extends BaseController {
                 try {
                     userService.checkUserStatus(Long.valueOf(qsId));
                     PosOrder orst = posOrderService.getById(posOrder.getId());
-                    if (orst.getState() == 3 && posOrder.getState() == 3) {
+                    if (orst.getDeliveryStatus() != null && orst.getDeliveryStatus() == 1L && posOrder.getDeliveryStatus() != null && posOrder.getDeliveryStatus() == 1L) {
                         throw new ServiceException(MessageUtils.message("no.order.snatched"));
                     }
-                    if (posOrder.getState() == 3) {
+                    if (posOrder.getDeliveryStatus() != null && posOrder.getDeliveryStatus() == 1L) {
                         QueryWrapper<PosOrder> wrapper = new QueryWrapper<>();
                         wrapper.eq("qs_id", qsId);
-                        wrapper.in("state", 3, 4);
+                        wrapper.in("delivery_status", 1, 2);
                         List<PosOrder> orsts = posOrderService.list(wrapper);
                         if (orsts.size() > 0) {
                             throw new ServiceException(MessageUtils.message("no.exist.undelivered.order"));
                         }
                     }
                     String userMessage = MessageUtils.message("no.message.push.delivery.personnel.receiving.order");
-                    if (posOrder.getState() == 4) {
+                    if (posOrder.getDeliveryStatus() != null && posOrder.getDeliveryStatus() == 2L) {
                         userMessage = MessageUtils.message("no.message.push.delivery.personnel.qspsz.order");
-                    } else if (posOrder.getState() == 12) {
+                    } else if (posOrder.getDeliveryStatus() != null && posOrder.getDeliveryStatus() == 3L) {
                         userMessage = MessageUtils.message("no.message.push.delivery.personnel.qsysd.order");
                     }
 
                     boolean org = posOrderService.saveOrUpdate(posOrder);
                     if (org) {
-                        //到付订单,设置用户账单为完成
-                        if (posOrder.getState() == 3 && "1".equals(orst.getCollectPayment())) {
+                        //到付订单,骑手送达时设置用户账单为完成
+                        if (posOrder.getDeliveryStatus() != null && posOrder.getDeliveryStatus() == 3L && "1".equals(orst.getCollectPayment())) {
                             updateUserBill(orst.getDdId(), orst.getUserId(), orst.getQsId());
                         }
                         InfoUser user = infoUserService.getById(orst.getUserId());
@@ -182,9 +183,9 @@ public class PosOrderQsOprateController extends BaseController {
                                 if (!StringUtils.isEmpty(user.getCid())) {
                                     String userTitle = "no.message.push.message";
                                     String userContent = "no.message.push.delivery.personnel.receiving.order";
-                                    if (posOrder.getState() == 4) {
+                                    if (posOrder.getDeliveryStatus() != null && posOrder.getDeliveryStatus() == 2L) {
                                         userContent = "no.message.push.delivery.personnel.qspsz.order";
-                                    } else if (posOrder.getState() == 12) {
+                                    } else if (posOrder.getDeliveryStatus() != null && posOrder.getDeliveryStatus() == 3L) {
                                         userContent = "no.message.push.delivery.personnel.qsysd.order";
                                     }
                                     final String finalUserContent = userContent;
@@ -201,7 +202,7 @@ public class PosOrderQsOprateController extends BaseController {
                                     });
                                 }
                                 // 给商家推送(骑手已接单时)
-                                if (!StringUtils.isEmpty(shu.getCid()) && posOrder.getState() == 3) {
+                                if (!StringUtils.isEmpty(shu.getCid()) && posOrder.getDeliveryStatus() != null && posOrder.getDeliveryStatus() == 1L) {
                                     String shTitle = "no.message.push.message";
                                     String shContent = "no.message.push.delivery.personnel.receiving.order";
                                     String shBody = OrderPushBodyDto.getJson(String.valueOf(orst.getDdId()), String.valueOf(posOrder.getState()));
@@ -258,4 +259,79 @@ public class PosOrderQsOprateController extends BaseController {
         }
     }
 
+    /**
+     * 骑手端订单列表(Tab分页)
+     */
+    @Anonymous
+    @Auth
+    @GetMapping("/orderList")
+    public AjaxResult orderList(@RequestHeader String token,
+                                @RequestParam(defaultValue = "1") int page,
+                                @RequestParam(defaultValue = "10") int size,
+                                @RequestParam String tab,
+                                @RequestParam(required = false) java.math.BigDecimal longitude,
+                                @RequestParam(required = false) java.math.BigDecimal latitude) {
+        JwtUtil jwtUtil = new JwtUtil();
+        String qsId = jwtUtil.getusid(token);
+        Long riderId = Long.valueOf(qsId);
+
+        LambdaQueryWrapper<PosOrder> wrapper = new LambdaQueryWrapper<>();
+
+        switch (tab) {
+            case "newTask":
+                wrapper.eq(PosOrder::getType, 0L)
+                       .eq(PosOrder::getDeliveryStatus, 0L)
+                       .eq(PosOrder::getState, 2L)
+                       .eq(PosOrder::getAfterSaleStatus, 0L);
+                if (longitude != null && latitude != null) {
+                    wrapper.last("ORDER BY ST_Distance_Sphere(point(longitude, latitude), point(" + longitude + ", " + latitude + ")) ASC");
+                } else {
+                    wrapper.orderByDesc(PosOrder::getCretim);
+                }
+                break;
+            case "toPickup":
+                wrapper.eq(PosOrder::getDeliveryStatus, 1L)
+                       .eq(PosOrder::getQsId, riderId)
+                       .eq(PosOrder::getAfterSaleStatus, 0L)
+                       .orderByAsc(PosOrder::getCretim);
+                break;
+            case "delivering":
+                wrapper.eq(PosOrder::getDeliveryStatus, 2L)
+                       .eq(PosOrder::getQsId, riderId)
+                       .eq(PosOrder::getAfterSaleStatus, 0L)
+                       .orderByAsc(PosOrder::getCretim);
+                break;
+            case "completed":
+                wrapper.eq(PosOrder::getState, 3L)
+                       .eq(PosOrder::getAfterSaleStatus, 0L)
+                       .eq(PosOrder::getQsId, riderId)
+                       .orderByDesc(PosOrder::getCretim);
+                break;
+            case "cancelled":
+                wrapper.eq(PosOrder::getState, 4L)
+                       .eq(PosOrder::getQsId, riderId)
+                       .eq(PosOrder::getAfterSaleStatus, 0L)
+                       .orderByDesc(PosOrder::getCretim);
+                break;
+            case "refund":
+                wrapper.gt(PosOrder::getAfterSaleStatus, 0L)
+                       .eq(PosOrder::getQsId, riderId)
+                       .orderByDesc(PosOrder::getCretim);
+                break;
+            default:
+                throw new ServiceException("无效的tab参数");
+        }
+
+        com.baomidou.mybatisplus.extension.plugins.pagination.Page<PosOrder> pageParam =
+                new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, size);
+        com.baomidou.mybatisplus.extension.plugins.pagination.Page<PosOrder> result = posOrderService.page(pageParam, wrapper);
+
+        java.util.Map<String, Object> data = new java.util.LinkedHashMap<>();
+        data.put("records", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("page", page);
+        data.put("size", size);
+        return success(data);
+    }
+
 }

+ 168 - 53
ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java

@@ -11,12 +11,15 @@ import com.ruoyi.app.utils.event.PushEventService;
 import com.ruoyi.common.annotation.Anonymous;
 import com.ruoyi.common.core.controller.BaseController;
 import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.framework.manager.AsyncManager;
 import com.ruoyi.system.domain.InfoUser;
 import com.ruoyi.system.domain.PosOrder;
 import com.ruoyi.system.service.IInfoUserService;
 import com.ruoyi.system.service.IPosOrderService;
+import com.ruoyi.system.utils.Auth;
+import com.ruoyi.system.utils.JwtUtil;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -41,29 +44,90 @@ public class PosOrderShOprateController extends BaseController {
     @Autowired
     private PushEventService pushEventService;
 
-
+    /**
+     * 商家接单:state 从 0 改为 1
+     */
+    @Anonymous
+    @Auth
+    @PostMapping("/acceptOrder")
+    public AjaxResult acceptOrder(@RequestHeader String token, @RequestParam Long id) {
+        PosOrder order = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getId, id));
+        if (order == null || order.getState() == null || order.getState() != 0L) {
+            throw new ServiceException("订单不存在或状态不允许接单");
+        }
+        PosOrder update = new PosOrder();
+        update.setId(order.getId());
+        update.setState(1L);
+        posOrderService.saveOrUpdate(update);
+        return AjaxResult.success();
+    }
 
     /**
-     * 商家出餐
-     * @param token
-     * @param id
-     * @return
+     * 商家出餐:state 从 1 改为 2
+     * 外送订单额外设置 deliveryStatus=0(等待骑手接单)
      */
     @Anonymous
+    @Auth
     @GetMapping("/dispatchOrder")
-    public AjaxResult dispatchOrder(@RequestHeader String token, @RequestParam Long id){
-        PosOrder order=posOrderService.getOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getId,id));
-        PosOrder update=new PosOrder();
+    public AjaxResult dispatchOrder(@RequestHeader String token, @RequestParam Long id) {
+        PosOrder order = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getId, id));
+        if (order == null || order.getState() == null || order.getState() != 1L) {
+            throw new ServiceException("订单不存在或状态不允许出餐");
+        }
+        PosOrder update = new PosOrder();
         update.setId(order.getId());
-        update.setDiningStatus(1L);
-        //自取、堂食
-        if(1L==order.getType() || 2L==order.getType()) {
-            update.setState(12L);
+        update.setState(2L);
+        // 外送订单:设置 deliveryStatus=0 等待骑手接单
+        if (order.getType() != null && order.getType() == 0L) {
+            update.setDeliveryStatus(0L);
+        }
+        posOrderService.saveOrUpdate(update);
+        // 出餐推送
+        chuCan(order, update);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 商家完成(自取/堂食):state 从 2 改为 3,payStatus 从 0 改为 1
+     */
+    @Anonymous
+    @Auth
+    @PostMapping("/completeOrder")
+    public AjaxResult completeOrder(@RequestHeader String token, @RequestParam Long id) {
+        PosOrder order = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getId, id));
+        if (order == null || order.getState() == null || order.getState() != 2L) {
+            throw new ServiceException("订单不存在或状态不允许完成");
         }
+        // 仅限自取(type=1)或堂食(type=2)
+        if (order.getType() == null || (order.getType() != 1L && order.getType() != 2L)) {
+            throw new ServiceException("仅自取和堂食订单支持此操作");
+        }
+        PosOrder update = new PosOrder();
+        update.setId(order.getId());
+        update.setState(3L);
+        update.setPayStatus(1L);
         posOrderService.saveOrUpdate(update);
-        if(0L==order.getType()){
-            chuCan(order,update);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 商家取消订单:校验 state IN (0,1),设 state=4
+     */
+    @Anonymous
+    @Auth
+    @PostMapping("/cancelOrder")
+    public AjaxResult cancelOrder(@RequestHeader String token, @RequestParam Long id) {
+        PosOrder order = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getId, id));
+        if (order == null || order.getState() == null) {
+            throw new ServiceException("订单不存在");
+        }
+        if (order.getState() != 0L && order.getState() != 1L) {
+            throw new ServiceException("当前状态不允许取消");
         }
+        PosOrder update = new PosOrder();
+        update.setId(order.getId());
+        update.setState(4L);
+        posOrderService.saveOrUpdate(update);
         return AjaxResult.success();
     }
 
@@ -71,53 +135,104 @@ public class PosOrderShOprateController extends BaseController {
      * 商家出餐推送:给用户和骑手推送通知
      */
     public void chuCan(PosOrder oldOrder, PosOrder posOrder) {
-        if (oldOrder.getDiningStatus() != null && oldOrder.getDiningStatus() == 0
-                && posOrder.getDiningStatus() != null && posOrder.getDiningStatus().equals(1L)) {
-            List<Long> ids = new ArrayList<>();
-            ids.add(oldOrder.getUserId());
-            if (oldOrder.getQsId() != null) {
-                ids.add(oldOrder.getQsId());
-            }
-            List<InfoUser> users = infoUserService.list(new LambdaQueryWrapper<InfoUser>().in(InfoUser::getUserId, ids));
-            Optional<InfoUser> user = users.stream().filter(x -> "0".equals(x.getUserType())).findFirst();
-            Optional<InfoUser> qsUser = users.stream().filter(x -> "2".equals(x.getUserType())).findFirst();
-
-            String title = "no.message.push.message";
-            String content = "no.message.push.merchant.ready.content";
-            Long stateVal = posOrder.getState() != null ? posOrder.getState() : oldOrder.getState();
-            String stateStr = stateVal != null ? String.valueOf(stateVal) : "2";
-            String body = OrderPushBodyDto.getJson(String.valueOf(oldOrder.getDdId()), stateStr);
-            String ddId = String.valueOf(oldOrder.getDdId());
-
-            // 给用户推送
-            if (user.isPresent()) {
-                InfoUser u = user.get();
+        List<Long> ids = new ArrayList<>();
+        ids.add(oldOrder.getUserId());
+        if (oldOrder.getQsId() != null) {
+            ids.add(oldOrder.getQsId());
+        }
+        List<InfoUser> users = infoUserService.list(new LambdaQueryWrapper<InfoUser>().in(InfoUser::getUserId, ids));
+        Optional<InfoUser> user = users.stream().filter(x -> "0".equals(x.getUserType())).findFirst();
+        Optional<InfoUser> qsUser = users.stream().filter(x -> "2".equals(x.getUserType())).findFirst();
+
+        String title = "no.message.push.message";
+        String content = "no.message.push.merchant.ready.content";
+        Long stateVal = posOrder.getState() != null ? posOrder.getState() : oldOrder.getState();
+        String stateStr = stateVal != null ? String.valueOf(stateVal) : "2";
+        String body = OrderPushBodyDto.getJson(String.valueOf(oldOrder.getDdId()), stateStr);
+        String ddId = String.valueOf(oldOrder.getDdId());
+
+        if (user.isPresent()) {
+            InfoUser u = user.get();
+            PayPush push = new PayPush();
+            Long uUserId = u.getUserId();
+            String uCid = u.getCid();
+            AsyncManager.me().execute(new TimerTask() {
+                @Override
+                public void run() {
+                    PayPush.userPushHandleLocal(push, pushEventService, uUserId, uCid, title, content, body, "", ddId);
+                }
+            });
+        }
+        if (qsUser.isPresent()) {
+            InfoUser qs = qsUser.get();
+            if (!StringUtils.isEmpty(qs.getCid())) {
                 PayPush push = new PayPush();
-                Long uUserId = u.getUserId();
-                String uCid = u.getCid();
+                Long qsUserId = qs.getUserId();
+                String qsCid = qs.getCid();
                 AsyncManager.me().execute(new TimerTask() {
                     @Override
                     public void run() {
-                        PayPush.userPushHandleLocal(push, pushEventService, uUserId, uCid, title, content, body, "", ddId);
+                        PayPush.qsPushHandleLocal(push, pushEventService, qsUserId, qsCid, title, content, body, "", ddId);
                     }
                 });
             }
-            // 给骑手推送
-            if (qsUser.isPresent()) {
-                InfoUser qs = qsUser.get();
-                if (!StringUtils.isEmpty(qs.getCid())) {
-                    PayPush push = new PayPush();
-                    Long qsUserId = qs.getUserId();
-                    String qsCid = qs.getCid();
-                    AsyncManager.me().execute(new TimerTask() {
-                        @Override
-                        public void run() {
-                            PayPush.qsPushHandleLocal(push, pushEventService, qsUserId, qsCid, title, content, body, "", ddId);
-                        }
-                    });
-                }
-            }
         }
     }
 
+    /**
+     * 商家端订单列表(Tab分页)
+     */
+    @Anonymous
+    @Auth
+    @GetMapping("/orderList")
+    public AjaxResult orderList(@RequestHeader String token,
+                                @RequestParam(defaultValue = "1") int page,
+                                @RequestParam(defaultValue = "10") int size,
+                                @RequestParam String tab,
+                                @RequestParam Long mdId,
+                                @RequestParam(required = false) String type) {
+        LambdaQueryWrapper<PosOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PosOrder::getMdId, mdId);
+
+        if (type != null && !type.isEmpty()) {
+            wrapper.eq(PosOrder::getType, Long.valueOf(type));
+        }
+
+        switch (tab) {
+            case "pending":
+                wrapper.eq(PosOrder::getState, 0L)
+                       .and(w -> w.eq(PosOrder::getPayStatus, 1L).or().in(PosOrder::getType, 1L, 2L));
+                break;
+            case "preparing":
+                wrapper.eq(PosOrder::getState, 1L);
+                break;
+            case "ready":
+                wrapper.eq(PosOrder::getState, 2L).eq(PosOrder::getAfterSaleStatus, 0L);
+                break;
+            case "completed":
+                wrapper.eq(PosOrder::getState, 3L).eq(PosOrder::getAfterSaleStatus, 0L);
+                break;
+            case "cancelled":
+                wrapper.eq(PosOrder::getState, 4L).eq(PosOrder::getAfterSaleStatus, 0L);
+                break;
+            case "refund":
+                wrapper.gt(PosOrder::getAfterSaleStatus, 0L);
+                break;
+            default:
+                throw new ServiceException("无效的tab参数");
+        }
+        wrapper.orderByDesc(PosOrder::getCretim);
+
+        com.baomidou.mybatisplus.extension.plugins.pagination.Page<PosOrder> pageParam =
+                new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, size);
+        com.baomidou.mybatisplus.extension.plugins.pagination.Page<PosOrder> result = posOrderService.page(pageParam, wrapper);
+
+        java.util.Map<String, Object> data = new java.util.LinkedHashMap<>();
+        data.put("records", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("page", page);
+        data.put("size", size);
+        return success(data);
+    }
+
 }

+ 23 - 8
ruoyi-admin/src/main/java/com/ruoyi/app/order/TestTask.java

@@ -53,6 +53,7 @@ public class TestTask {
 
     /**
      * 订单未支付,超过一定时间后取消
+     * 新逻辑:查 payStatus=0 AND state=0 且超时,设 state=4(已取消)
      * @param minute
      */
     public void testTiming(Integer minute){
@@ -61,11 +62,11 @@ public class TestTask {
         for(int i=0;i<ordlist.size();i++){
             PosOrder posOrder = new PosOrder();
             posOrder.setId(ordlist.get(i).getId());
-            posOrder.setState(10L);
+            posOrder.setState(4L); // state=4 已取消(替换旧 state=10)
             posOrderService.saveOrUpdate(posOrder);
             try {
-                if(posOrder.getPoints()!=null && posOrder.getPoints()>0){
-                    posorder.returnPoints(posOrder.getUserId(), Long.valueOf(posOrder.getDdId()),Long.valueOf(posOrder.getPoints()));
+                if(ordlist.get(i).getPoints()!=null && ordlist.get(i).getPoints()>0){
+                    posorder.returnPoints(ordlist.get(i).getUserId(), Long.valueOf(ordlist.get(i).getDdId()),Long.valueOf(ordlist.get(i).getPoints()));
                 }
             }catch (Exception e){
                 logger.warn("定时任务取消订单,积分返还失败订单号: {},异常信息:{}", posOrder.getId(),e.getMessage());
@@ -82,10 +83,12 @@ public class TestTask {
 
     /**
      * 退款处理中,查询zalopay退款结果,设置退款状态
+     * 新逻辑:查 afterSaleStatus=2(退款中),退款成功后设 afterSaleStatus=6(售后完成)
+     * 注意:本次不实现完整退款流程,保留方法结构
      */
    public void refundProcessing() throws URISyntaxException, IOException {
         LambdaQueryWrapper<PosOrder> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.eq(PosOrder::getState,13L); //13退款处理
+        queryWrapper.eq(PosOrder::getAfterSaleStatus, 2L); // afterSaleStatus=2 退款
         List<PosOrder> list = posOrderMapper.selectList(queryWrapper);
         System.out.println("定时任务查询退款处理中的数据,数量: "+list.size());
        logger.info("定时任务查询退款处理中的数据,数量: {}", list.size());
@@ -104,7 +107,7 @@ public class TestTask {
                 logger.warn("定时任务处理退款结果,订单号: {}", list.get(i).getDdId());
                 PosOrder posOrder = new PosOrder();
                 posOrder.setId(list.get(i).getId());
-                posOrder.setState(11L); //11售后完成
+                posOrder.setAfterSaleStatus(6L); // afterSaleStatus=6 售后完成(替换旧 state=11)
                 posOrderService.saveOrUpdate(posOrder);
             }
         }
@@ -112,16 +115,28 @@ public class TestTask {
     }
 
     /**
-     * 订单送达,超过一定时间后自动设置为完成
+     * 自动完成兜底:state=2 AND afterSaleStatus=0 且超时,自动设 state=3
+     * 外送同时设 deliveryStatus=3,自取/堂食同时设 payStatus=1
      * @param minute
      */
     public void zidwancheng(Integer minute){
         System.out.println("间隔时间:"+minute+"分钟");
-        List<PosOrder> ordlist = posOrderMapper.getOverOrderTim(60,minute);
+        LambdaQueryWrapper<PosOrder> query = new LambdaQueryWrapper<>();
+        query.eq(PosOrder::getState, 2L)
+             .eq(PosOrder::getAfterSaleStatus, 0L)
+             .apply("TIMESTAMPDIFF(MINUTE, cretim) > {0}", minute);
+        List<PosOrder> ordlist = posOrderService.list(query);
         for(int i=0;i<ordlist.size();i++){
             PosOrder pos = new PosOrder();
             pos.setId(ordlist.get(i).getId());
-            pos.setState(5L);
+            pos.setState(3L);
+            // 外送订单自动完成 deliveryStatus
+            if(ordlist.get(i).getType() != null && ordlist.get(i).getType() == 0L){
+                pos.setDeliveryStatus(3L);
+            } else {
+                // 自取/堂食自动完成 payStatus
+                pos.setPayStatus(1L);
+            }
             posOrderService.saveOrUpdate(pos);
             PosOrder order = posOrderService.getById(ordlist.get(i).getId());
             posorder.setSanghuBilling(order);

+ 82 - 1
ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java

@@ -138,7 +138,6 @@ public class UserOrderController extends BaseController {
             posOrder.setDdId(subddId);
             posOrder.setParentDdId(input.getDdId().toString()); // 设置父订单ID
             posOrder.setCretim(new Date());
-            posOrder.setState(0L); // 初始状态
             // 根据item设置订单信息
             posOrder.setShId(item.getShId());
             posOrder.setMdId(item.getMdId());
@@ -166,6 +165,16 @@ public class UserOrderController extends BaseController {
             posOrder.setFoodAmount(item.getFoodAmount());
             posOrder.setPickUpTime(input.getPickUpTime());
             posOrder.setReservePhone(input.getReservePhone());
+            // 新状态字段初始化
+            posOrder.setState(0L);
+            posOrder.setAfterSaleStatus(0L);
+            posOrder.setDeliveryStatus(null);
+            // 外送到付(payType=1):payStatus=1;其他:payStatus=0
+            if (item.getType() != null && item.getType() == 0 && "1".equals(input.getPaymentMethod())) {
+                posOrder.setPayStatus(1L);
+            } else {
+                posOrder.setPayStatus(0L);
+            }
            Optional<PosStore> storeOptional = storeList.stream()
                    .filter(x -> item.getMdId().equals(Long.valueOf(x.getId())))
                    .findFirst();
@@ -319,5 +328,77 @@ public class UserOrderController extends BaseController {
         return success(distance);
     }
 
+    /**
+     * 用户端订单列表(Tab分页)
+     */
+    @GetMapping("/orderList")
+    @Auth
+    @Anonymous
+    public AjaxResult orderList(@RequestHeader String token,
+                                @RequestParam(defaultValue = "1") int page,
+                                @RequestParam(defaultValue = "10") int size,
+                                @RequestParam String tab,
+                                @RequestParam(required = false) String type) {
+        JwtUtil jwtUtil = new JwtUtil();
+        String userId = jwtUtil.getusid(token);
+
+        LambdaQueryWrapper<PosOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PosOrder::getUserId, Long.valueOf(userId));
+
+        // 类型筛选
+        if (type != null && !type.isEmpty()) {
+            wrapper.eq(PosOrder::getType, Long.valueOf(type));
+        }
+
+        switch (tab) {
+            case "unpaid":
+                wrapper.eq(PosOrder::getPayStatus, 0L).eq(PosOrder::getType, 0L);
+                break;
+            case "active":
+                wrapper.in(PosOrder::getState, 0L, 1L, 2L)
+                       .eq(PosOrder::getAfterSaleStatus, 0L)
+                       .and(w -> w.eq(PosOrder::getPayStatus, 1L).or().in(PosOrder::getType, 1L, 2L));
+                break;
+            case "completed":
+                wrapper.eq(PosOrder::getState, 3L).eq(PosOrder::getAfterSaleStatus, 0L);
+                break;
+            case "cancelled":
+                wrapper.eq(PosOrder::getState, 4L).eq(PosOrder::getAfterSaleStatus, 0L);
+                break;
+            case "refund":
+                wrapper.gt(PosOrder::getAfterSaleStatus, 0L);
+                break;
+            default:
+                throw new ServiceException("无效的tab参数");
+        }
+        wrapper.orderByDesc(PosOrder::getCretim);
+
+        // 分页
+        com.baomidou.mybatisplus.extension.plugins.pagination.Page<PosOrder> pageParam =
+                new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, size);
+        com.baomidou.mybatisplus.extension.plugins.pagination.Page<PosOrder> result = posOrderService.page(pageParam, wrapper);
+
+        // 附加关联数据
+        for (PosOrder order : result.getRecords()) {
+            enrichOrderData(order);
+        }
+
+        java.util.Map<String, Object> data = new java.util.LinkedHashMap<>();
+        data.put("records", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("page", page);
+        data.put("size", size);
+        return success(data);
+    }
+
+    private void enrichOrderData(PosOrder order) {
+        try {
+            if (order.getShId() != null) {
+                InfoUser shanghu = infoUserService.getById(order.getShId());
+                order.setPriceAll(shanghu != null ? String.valueOf(shanghu.getNickName()) : null);
+            }
+        } catch (Exception ignored) {}
+    }
+
 
 }

+ 2 - 1
ruoyi-admin/src/main/java/com/ruoyi/app/pay/PayController.java

@@ -261,7 +261,8 @@ public class PayController extends BaseController {
                                 if (posOrder.getState() == 0) {
                                     PosOrder order = new PosOrder();
                                     order.setId(posOrder.getId());
-                                    order.setState(1L);
+                                    order.setState(0L); // 保持 state=0(待处理),支付成功不改订单状态
+                                    order.setPayStatus(1L); // 支付状态设为已支付
                                     posOrderService.saveOrUpdate(order);
                                     QueryWrapper<UserBilling> wrapper = new QueryWrapper<>();
                                     wrapper.eq("dd_id", posOrder.getDdId());

+ 19 - 1
ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrder.java

@@ -135,7 +135,9 @@ public class PosOrder {
 
     /**
      * 出餐状态
+     * @deprecated 由 state=2(已出餐)替代
      */
+    @Deprecated
     @Excel(name = "出餐状态")
     private Long diningStatus;
 
@@ -310,7 +312,10 @@ public class PosOrder {
     private String pickUpTime;
     /** 自取预留电话 */
     private String reservePhone;
-    /** 商家是否已接单 */
+    /** 商家是否已接单
+     * @deprecated 由 state=1(已接单)替代
+     */
+    @Deprecated
     private Boolean isAccepted;
 
     /** 是否显示 */
@@ -322,6 +327,19 @@ public class PosOrder {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date displayTime;
 
+    /**
+     * 配送状态:0待接单,1骑手已接单,2配送中,3已送达(仅外送订单)
+     */
+    private Long deliveryStatus;
+
+    /**
+     * 支付状态:0未支付,1已支付,2已退款
+     */
+    private Long payStatus;
 
+    /**
+     * 售后状态:0无售后,1申请中,2退款中,3已退款,4退款拒绝,5客服介入,6售后完成
+     */
+    private Long afterSaleStatus;
 
 }

+ 16 - 1
ruoyi-system/src/main/resources/mapper/system/PosOrderMapper.xml

@@ -57,10 +57,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="isAccepted" column="is_accepted" />
         <result property="isDisplay" column="is_display" />
         <result property="displayTime" column="display_time" />
+        <result property="deliveryStatus" column="delivery_status" />
+        <result property="payStatus" column="pay_status" />
+        <result property="afterSaleStatus" column="after_sale_status" />
     </resultMap>
 
     <sql id="selectPosOrderVo">
-        select id, dd_id, sh_id, md_id, cretim, shdz_id, user_id,sh_address, amount, remarks, state, type, delry_time,food,yh_id,yh_name,md_yh_id,md_yh_name,md_discount_amount,jvli,freight,dining_status,qs_id,pay_url,collect_payment,activity,md_activity,kefu_state,kefu_content,kefu_repeat,repeat_dd_id,sales_name,md_sales_name,sales_reduction,md_sales_reduction,points,points_reduction,sd_time,pay_type,parent_dd_id,order_category,table_id,table_no,logo,pos_name,food_amount,pick_up_num,pick_up_time,reserve_phone,is_accepted,is_display,display_time from pos_order
+        select id, dd_id, sh_id, md_id, cretim, shdz_id, user_id,sh_address, amount, remarks, state, type, delry_time,food,yh_id,yh_name,md_yh_id,md_yh_name,md_discount_amount,jvli,freight,dining_status,qs_id,pay_url,collect_payment,activity,md_activity,kefu_state,kefu_content,kefu_repeat,repeat_dd_id,sales_name,md_sales_name,sales_reduction,md_sales_reduction,points,points_reduction,sd_time,pay_type,parent_dd_id,order_category,table_id,table_no,logo,pos_name,food_amount,pick_up_num,pick_up_time,reserve_phone,is_accepted,is_display,display_time,delivery_status,pay_status,after_sale_status from pos_order
     </sql>
 
     <select id="selectPosOrderList" parameterType="PosOrder" resultMap="PosOrderResult">
@@ -97,6 +100,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="tableId != null"> and table_id = #{tableId}</if>
             <if test="logo != null and logo != ''"> and logo = #{logo}</if>
             <if test="posName != null and posName != ''"> and pos_name = #{posName}</if>
+            <if test="payStatus != null"> and pay_status = #{payStatus}</if>
+            <if test="deliveryStatus != null"> and delivery_status = #{deliveryStatus}</if>
+            <if test="afterSaleStatus != null"> and after_sale_status = #{afterSaleStatus}</if>
             and not (state=0 and collect_payment=0)
         </where>
         order by id desc
@@ -186,6 +192,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isAccepted != null">is_accepted,</if>
             <if test="isDisplay != null">is_display,</if>
             <if test="displayTime != null">display_time,</if>
+            <if test="deliveryStatus != null">delivery_status,</if>
+            <if test="payStatus != null">pay_status,</if>
+            <if test="afterSaleStatus != null">after_sale_status,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="ddId != null">#{ddId},</if>
@@ -226,6 +235,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isAccepted != null">#{isAccepted},</if>
             <if test="isDisplay != null">#{isDisplay},</if>
             <if test="displayTime != null">#{displayTime},</if>
+            <if test="deliveryStatus != null">#{deliveryStatus},</if>
+            <if test="payStatus != null">#{payStatus},</if>
+            <if test="afterSaleStatus != null">#{afterSaleStatus},</if>
          </trim>
     </insert>
 
@@ -272,6 +284,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isAccepted != null">is_accepted = #{isAccepted},</if>
             <if test="isDisplay != null">is_display = #{isDisplay},</if>
             <if test="displayTime != null">display_time = #{displayTime},</if>
+            <if test="deliveryStatus != null">delivery_status = #{deliveryStatus},</if>
+            <if test="payStatus != null">pay_status = #{payStatus},</if>
+            <if test="afterSaleStatus != null">after_sale_status = #{afterSaleStatus},</if>
         </trim>
         where id = #{id}
     </update>

+ 143 - 0
specs/006-orderstate/contracts/order-list-apis.md

@@ -0,0 +1,143 @@
+# Contracts: 订单列表接口
+
+三个新列表接口,统一返回 `{ records, total, page, size }` 格式。
+
+## 1. 用户端订单列表
+
+**Endpoint**: `GET /system/userOrder/orderList`
+**Auth**: 用户 token (Header)
+
+### 请求参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| page | int | 是 | 页码,从1开始 |
+| size | int | 是 | 每页条数 |
+| tab | String | 是 | unpaid / active / completed / cancelled / refund |
+| type | String | 否 | 0外送 / 1自取 / 2堂食,不传查所有 |
+
+### Tab 查询条件
+
+| tab | 条件 |
+|-----|------|
+| unpaid | `payStatus=0 AND type=0` |
+| active | `state IN (0,1,2) AND afterSaleStatus=0 AND (payStatus=1 OR type IN (1,2))` |
+| completed | `state=3 AND afterSaleStatus=0` |
+| cancelled | `state=4 AND afterSaleStatus=0` |
+| refund | `afterSaleStatus > 0` |
+
+### 响应格式
+
+```json
+{
+  "code": 200,
+  "msg": "操作成功",
+  "data": {
+    "records": [{ /* PosOrder 全字段 + shanghu + store + shaddress + user + food + parentRemarks */ }],
+    "total": 100,
+    "page": 1,
+    "size": 10
+  }
+}
+```
+
+---
+
+## 2. 商家端订单列表
+
+**Endpoint**: `GET /system/orderShOprate/orderList`
+**Auth**: 商家 token (Header)
+
+### 请求参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| page | int | 是 | 页码 |
+| size | int | 是 | 每页条数 |
+| tab | String | 是 | pending / preparing / ready / completed / cancelled / refund |
+| mdId | String | 是 | 门店ID |
+| type | String | 否 | 订单类型筛选 |
+
+### Tab 查询条件
+
+| tab | 条件 |
+|-----|------|
+| pending | `state=0 AND (payStatus=1 OR type IN (1,2)) AND mdId=?` |
+| preparing | `state=1 AND mdId=?` |
+| ready | `state=2 AND afterSaleStatus=0 AND mdId=?` |
+| completed | `state=3 AND afterSaleStatus=0 AND mdId=?` |
+| cancelled | `state=4 AND afterSaleStatus=0 AND mdId=?` |
+| refund | `afterSaleStatus > 0 AND mdId=?` |
+
+---
+
+## 3. 骑手端订单列表
+
+**Endpoint**: `GET /system/orderQsOprate/orderList`
+**Auth**: 骑手 token (Header,JwtUtil.getusid 即 qsId)
+
+### 请求参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| page | int | 是 | 页码 |
+| size | int | 是 | 每页条数 |
+| tab | String | 是 | newTask / toPickup / delivering / completed / cancelled / refund |
+| longitude | BigDecimal | 条件必填 | newTask Tab 必填 |
+| latitude | BigDecimal | 条件必填 | newTask Tab 必填 |
+
+### Tab 查询条件
+
+| tab | 条件 | 排序 |
+|-----|------|------|
+| newTask | `type=0 AND deliveryStatus=0 AND state=2 AND afterSaleStatus=0` | 距离 ASC |
+| toPickup | `deliveryStatus=1 AND qsId=当前骑手 AND afterSaleStatus=0` | 时间 ASC |
+| delivering | `deliveryStatus=2 AND qsId=当前骑手 AND afterSaleStatus=0` | 时间 ASC |
+| completed | `state=3 AND afterSaleStatus=0 AND qsId=当前骑手` | 时间 DESC |
+| cancelled | `state=4 AND qsId=当前骑手 AND afterSaleStatus=0` | 时间 DESC |
+| refund | `afterSaleStatus > 0 AND qsId=当前骑手` | 时间 DESC |
+
+---
+
+## 商家操作接口改造
+
+### 接单
+
+**Endpoint**: 现有 `PosOrderShOprateController` 接单方法
+**变更**: `state` 从 0 改为 1
+
+### 出餐
+
+**Endpoint**: 现有 `dispatchOrder()`
+**变更**:
+- `state` 从 1 改为 2
+- 外送订单:额外设置 `deliveryStatus=0`
+- 自取/堂食:只改 `state=2`
+
+### 完成(自取/堂食)
+
+**Endpoint**: 新增方法
+**变更**: `state` 从 2 改为 3,`payStatus` 从 0 改为 1
+
+## 骑手操作接口改造
+
+### 接单 acceptOrder
+
+**变更**: 校验 `type=0 AND afterSaleStatus=0`,设置 `deliveryStatus=1`
+
+### 取餐 pickupOrder
+
+**变更**: 校验 `type=0 AND afterSaleStatus=0`,设置 `deliveryStatus=2`
+
+### 送达 deliverOrder
+
+**变更**: 设置 `deliveryStatus=3`,同时设置 `state=3`
+
+## 订单创建改造
+
+**Endpoint**: 现有 `UserOrderController.createOrder()`
+**变更**:
+- 所有类型:`state=0`、`afterSaleStatus=0`、`deliveryStatus=NULL`
+- 外送到付(type=0, payType=1):`payStatus=1`
+- 自取/堂食现金(type=1/2):`payStatus=0`
+- 在线支付:`payStatus=0`

+ 80 - 0
specs/006-orderstate/data-model.md

@@ -0,0 +1,80 @@
+# Data Model: 订单状态四字段分离
+
+## 变更的实体
+
+### PosOrder(pos_order 表)
+
+#### 新增字段
+
+| 字段名 | Java 类型 | 数据库列 | 数据库类型 | 默认值 | 说明 |
+|--------|----------|---------|-----------|-------|------|
+| `deliveryStatus` | Long | `delivery_status` | BIGINT | NULL | 配送状态:0待接单,1骑手已接单,2配送中,3已送达。自取/堂食为 NULL |
+| `payStatus` | Long | `pay_status` | BIGINT | 0 | 支付状态:0未支付,1已支付,2已退款 |
+| `afterSaleStatus` | Long | `after_sale_status` | BIGINT | 0 | 售后状态:0无售后,1申请中,2退款中,3已退款,4退款拒绝,5客服介入,6售后完成 |
+
+#### 重新定义的字段
+
+| 字段名 | 旧值 → 新值 | 说明 |
+|--------|-----------|------|
+| `state` | 0-13 → 0-4 | 旧值废弃,新值:0待处理,1已接单,2已出餐,3已完成,4已取消 |
+
+#### 废弃但保留的字段
+
+| 字段名 | 替代方案 |
+|--------|---------|
+| `diningStatus` | 由 state=2(已出餐)替代 |
+| `isAccepted` | 由 state=1(已接单)替代 |
+
+## 状态机定义
+
+### state(订单状态)— 所有类型共用
+
+```
+0(待处理) → 1(已接单) → 2(已出餐) → 3(已完成)
+0(待处理) → 4(已取消)
+1(已接单) → 4(已取消)
+```
+
+转换规则:
+- 0→1:商家接单
+- 1→2:商家出餐
+- 2→3:骑手送达(外送) / 商家确认完成(自取/堂食) / 自动完成兜底
+- *→4:取消(商家/系统/用户超时/全额退款)
+
+### deliveryStatus(配送状态)— 仅 type=0 外送
+
+```
+0(待接单) → 1(骑手已接单) → 2(配送中) → 3(已送达)
+```
+
+- 商家出餐时设 deliveryStatus=0
+- 自取/堂食始终为 NULL
+
+### payStatus(支付状态)— 所有类型共用
+
+```
+0(未支付) → 1(已支付)
+1(已支付) → 2(已退款)
+```
+
+- 外送到付:创建时直接 payStatus=1
+- 自取/堂食现金:创建时 payStatus=0,商家收款+完成时设 1
+- 在线支付:支付回调后设 1
+
+### afterSaleStatus(售后状态)— 独立状态机
+
+```
+0(无售后) → 1(申请中) → 2(退款中) → 3(已退款) → 6(售后完成)
+                       → 4(退款拒绝) → 5(客服介入) → 6(售后完成)
+```
+
+- 本次不实现退款流程(afterSaleStatus 预留,值保持 0)
+- 用户/商家取消订单时 afterSaleStatus 保持 0
+
+## 各类型订单字段使用
+
+| 订单类型 | state | deliveryStatus | payStatus | afterSaleStatus |
+|---------|-------|----------------|-----------|-----------------|
+| 外送(type=0) | 0→1→2→3 | 0→1→2→3 | 0→1 或 创建时1 | 0(预留) |
+| 自取(type=1) | 0→1→2→3 | NULL | 0→1(完成时) | 0(预留) |
+| 堂食(type=2) | 0→1→2→3 | NULL | 0→1(完成时) | 0(预留) |

+ 194 - 0
specs/006-orderstate/frontend-api-guide.md

@@ -0,0 +1,194 @@
+# 订单列表接口前端对接文档
+
+## 通用说明
+
+- 三个端各自独立的 `orderList` 接口,通过 `tab` 参数区分 Tab
+- 返回格式统一:`{ code, msg, data: { records, total, page, size } }`
+- `records` 中每条数据为 PosOrder 全字段,新增字段:
+  - `deliveryStatus` — 配送状态(Long,自取/堂食为 null)
+  - `payStatus` — 支付状态(Long)
+  - `afterSaleStatus` — 售后状态(Long)
+- **状态展示文字由前端根据 `type + state + deliveryStatus + payStatus + afterSaleStatus` 组合计算**,后端不返回 statusText
+- 所有接口需要 `token` Header
+
+---
+
+## 一、用户端
+
+**接口**: `GET /system/userOrder/orderList`
+
+### 参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| token | Header | 是 | 用户身份 |
+| page | int | 是 | 页码,从 1 开始 |
+| size | int | 是 | 每页条数 |
+| tab | String | 是 | 见下表 |
+| type | String | 否 | 订单类型筛选:`0` 外送 / `1` 自取 / `2` 堂食,不传查所有 |
+
+### Tab 列表
+
+| Tab 名称 | tab 值 | 含义 | 请求示例 |
+|----------|--------|------|---------|
+| 待付款 | `unpaid` | 仅外送未支付订单 | `?tab=unpaid&page=1&size=10` |
+| 进行中 | `active` | 进行中的订单 | `?tab=active&page=1&size=10` |
+| 已完成 | `completed` | 已完成订单 | `?tab=completed&page=1&size=10` |
+| 已取消 | `cancelled` | 已取消订单 | `?tab=cancelled&page=1&size=10` |
+| 退款/售后 | `refund` | 有售后记录的订单 | `?tab=refund&page=1&size=10` |
+
+### 前端展示文字对照表
+
+根据返回字段组合显示用户看到的文字:
+
+| type | state | payStatus | deliveryStatus | afterSaleStatus | 显示文字 |
+|------|-------|-----------|----------------|-----------------|---------|
+| 0(外送) | 0 | 0 | - | 0 | 待付款 |
+| 0(外送) | 0 | 1 | - | 0 | 待商家确认 |
+| 0(外送) | 1 | 1 | - | 0 | 商家备餐中 |
+| 0(外送) | 2 | 1 | 0 | 0 | 待骑手配送 |
+| 0(外送) | 2 | 1 | 1 | 0 | 骑手已接单 |
+| 0(外送) | 2 | 1 | 2 | 0 | 配送中 |
+| 0(外送) | 2 | 1 | 3 | 0 | 已送达 |
+| 0(外送) | 3 | 1 | - | 0 | 已完成 |
+| 0(外送) | * | * | * | 1 | 退款申请中 |
+| 0(外送) | * | * | * | 2 | 退款中 |
+| 0(外送) | 4 | 2 | - | 3 | 已退款 |
+| 0(外送) | 4 | - | - | 0 | 已取消 |
+| 1(自取) | 0 | 0 | null | 0 | 待商家确认 |
+| 1(自取) | 1 | 1 | null | 0 | 商家备餐中 |
+| 1(自取) | 2 | 1 | null | 0 | 待取餐(请到店取餐) |
+| 1(自取) | 3 | 1 | null | 0 | 已完成 |
+| 1(自取) | 4 | - | null | 0 | 已取消 |
+| 2(堂食) | 0 | 0 | null | 0 | 待商家确认 |
+| 2(堂食) | 1 | 1 | null | 0 | 商家备餐中 |
+| 2(堂食) | 2 | 1 | null | 0 | 备餐完成 |
+| 2(堂食) | 3 | 1 | null | 0 | 已完成 |
+| 2(堂食) | 4 | - | null | 0 | 已取消 |
+
+> `*` 表示该字段值不影响展示文字,优先展示 afterSaleStatus 对应文字
+
+---
+
+## 二、商家端
+
+**接口**: `GET /system/orderShOprate/orderList`
+
+### 参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| token | Header | 是 | 商家身份 |
+| page | int | 是 | 页码,从 1 开始 |
+| size | int | 是 | 每页条数 |
+| tab | String | 是 | 见下表 |
+| mdId | Long | 是 | 门店 ID |
+| type | String | 否 | 订单类型筛选:`0` / `1` / `2`,不传显示全部 |
+
+### Tab 列表
+
+| Tab 名称 | tab 值 | 含义 | 请求示例 |
+|----------|--------|------|---------|
+| 待受理 | `pending` | 已支付待接单(自取/堂食下单即可见) | `?tab=pending&page=1&size=10&mdId=100` |
+| 待出餐 | `preparing` | 已接单备餐中 | `?tab=preparing&page=1&size=10&mdId=100` |
+| 已出餐 | `ready` | 出餐完成待取/待配送 | `?tab=ready&page=1&size=10&mdId=100` |
+| 已完成 | `completed` | 已完成订单 | `?tab=completed&page=1&size=10&mdId=100` |
+| 已取消 | `cancelled` | 已取消订单 | `?tab=cancelled&page=1&size=10&mdId=100` |
+| 退款/售后 | `refund` | 有售后记录的订单 | `?tab=refund&page=1&size=10&mdId=100` |
+
+### 商家端操作接口
+
+| 操作 | 接口 | 方法 | 参数 | 说明 |
+|------|------|------|------|------|
+| 接单 | `/system/orderShOprate/acceptOrder` | POST | `id`(订单ID) | state 0→1 |
+| 出餐 | `/system/orderShOprate/dispatchOrder` | GET | `id`(订单ID) | state 1→2,外送额外设 deliveryStatus=0 |
+| 完成(自取/堂食) | `/system/orderShOprate/completeOrder` | POST | `id`(订单ID) | state 2→3,payStatus 0→1 |
+| 取消 | `/system/orderShOprate/cancelOrder` | POST | `id`(订单ID) | state→4,仅限 state=0 或 1 |
+
+---
+
+## 三、骑手端
+
+**接口**: `GET /system/orderQsOprate/orderList`
+
+### 参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| token | Header | 是 | 骑手身份(JwtUtil 解析即 qsId) |
+| page | int | 是 | 页码,从 1 开始 |
+| size | int | 是 | 每页条数 |
+| tab | String | 是 | 见下表 |
+| longitude | BigDecimal | 条件必填 | 骑手当前经度(**newTask Tab 必填**) |
+| latitude | BigDecimal | 条件必填 | 骑手当前纬度(**newTask Tab 必填**) |
+
+### Tab 列表
+
+| Tab 名称 | tab 值 | 含义 | 排序 | 请求示例 |
+|----------|--------|------|------|---------|
+| 新任务 | `newTask` | 可抢的外送订单 | 距离从近到远 | `?tab=newTask&page=1&size=10&longitude=106.7&latitude=10.8` |
+| 待取货 | `toPickup` | 已接单待取餐 | 时间从旧到新 | `?tab=toPickup&page=1&size=10` |
+| 配送中 | `delivering` | 正在配送 | 时间从旧到新 | `?tab=delivering&page=1&size=10` |
+| 已完成 | `completed` | 已完成订单 | 时间从新到旧 | `?tab=completed&page=1&size=10` |
+| 已取消 | `cancelled` | 已取消订单 | 时间从新到旧 | `?tab=cancelled&page=1&size=10` |
+| 退款/售后 | `refund` | 有售后记录的订单 | 时间从新到旧 | `?tab=refund&page=1&size=10` |
+
+### 骑手端操作接口
+
+| 操作 | 接口 | 方法 | 参数 | 说明 |
+|------|------|------|------|------|
+| 接单 | `/system/orderQsOprate/acceptOrder` | POST | `id`(订单ID) | deliveryStatus→1,绑定 qsId |
+| 取餐 | `/system/orderQsOprate/pickupOrder` | POST | `id`(订单ID) | deliveryStatus→2 |
+| 送达 | `/system/orderQsOprate/deliverOrder` | POST | `id`(订单ID) | deliveryStatus→3,state→3 |
+
+---
+
+## 四、字段值速查
+
+### state(订单状态)
+
+| 值 | 含义 |
+|----|------|
+| 0 | 待处理 |
+| 1 | 已接单 |
+| 2 | 已出餐 |
+| 3 | 已完成 |
+| 4 | 已取消 |
+
+### deliveryStatus(配送状态,仅外送 type=0)
+
+| 值 | 含义 |
+|----|------|
+| null | 自取/堂食不适用 |
+| 0 | 待接单(等待骑手) |
+| 1 | 骑手已接单 |
+| 2 | 配送中 |
+| 3 | 已送达 |
+
+### payStatus(支付状态)
+
+| 值 | 含义 |
+|----|------|
+| 0 | 未支付 |
+| 1 | 已支付 |
+| 2 | 已退款 |
+
+### afterSaleStatus(售后状态)
+
+| 值 | 含义 |
+|----|------|
+| 0 | 无售后 |
+| 1 | 申请中 |
+| 2 | 退款中 |
+| 3 | 已退款 |
+| 4 | 退款拒绝 |
+| 5 | 客服介入 |
+| 6 | 售后完成 |
+
+### type(订单类型)
+
+| 值 | 含义 |
+|----|------|
+| 0 | 外送 |
+| 1 | 自取 |
+| 2 | 堂食 |

+ 86 - 0
specs/006-orderstate/plan.md

@@ -0,0 +1,86 @@
+# Implementation Plan: 订单状态四字段分离
+
+**Branch**: `test` | **Date**: 2026-05-15 | **Spec**: [spec.md](spec.md)
+**Input**: Feature specification from `/specs/006-orderstate/spec.md`
+
+## Summary
+
+将现有 `pos_order` 表中混用的 `state` 字段拆分为四个独立字段(`state`、`delivery_status`、`pay_status`、`after_sale_status`),参考美团订单状态机设计。涉及数据库变更、实体类扩展、MyBatis Mapper 更新、三端(用户/商家/骑手)操作接口改造、新订单列表接口开发、定时任务调整、以及平台管理端前端适配。
+
+## Technical Context
+
+**Language/Version**: Java 21
+**Primary Dependencies**: Spring Boot 3.3.5, MyBatis 3.0.3 (mybatis-spring-boot-starter),若依框架
+**Storage**: MySQL
+**Testing**: 手动测试(项目无自动化测试框架)
+**Target Platform**: Linux/Windows Server
+**Project Type**: Web Service (REST API + 管理端前端)
+**Performance Goals**: 无特殊性能要求,常规 CRUD
+**Constraints**: 数据库变更写 SQL 脚本(`updatesql/sql.md`),不直接执行
+**Scale/Scope**: 3 个 Controller 改造 + 3 个新列表接口 + 1 个前端项目调整
+
+### 关键文件清单
+
+| 文件 | 路径 | 变更类型 |
+|------|------|---------|
+| PosOrder.java | `ruoyi-system/.../domain/PosOrder.java` | 新增3个字段 |
+| PosOrderMapper.xml | `ruoyi-system/.../mapper/system/PosOrderMapper.xml` | resultMap + SQL 更新 |
+| PosOrderShOprateController | `ruoyi-admin/.../app/order/PosOrderShOprateController.java` | 改造接单/出餐 + 新增列表接口 |
+| PosOrderQsOprateController | `ruoyi-admin/.../app/order/PosOrderQsOprateController.java` | 改造骑手操作 + 新增列表接口 |
+| UserOrderController | `ruoyi-admin/.../app/order/UserOrderController.java` | 改造创建订单 + 新增列表接口 |
+| OrderAppealController | `ruoyi-admin/.../app/order/OrderAppealController.java` | 改造取消订单 |
+| TestTask.java | `ruoyi-admin/.../app/order/TestTask.java` | 定时任务改造 |
+| sql.md | `updatesql/sql.md` | 新增 ALTER TABLE 语句 |
+| 平台管理前端 | `E:\QtwCode\foodie\foodie-admin-vue\src\views\system\order\index.vue` | 筛选/展示改造 |
+
+## Constitution Check
+
+*Constitution 为模板状态,无自定义约束。跳过 gate 检查。*
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/test/
+├── plan.md              # This file
+├── research.md          # Phase 0 output
+├── data-model.md        # Phase 1 output
+├── quickstart.md        # Phase 1 output
+├── contracts/           # Phase 1 output
+└── tasks.md             # Phase 2 output (via /speckit.tasks)
+```
+
+### Source Code (repository root)
+
+```text
+ruoyi-system/src/main/java/com/ruoyi/system/
+├── domain/PosOrder.java                  # 实体类
+├── mapper/PosOrderMapper.java            # Mapper接口
+└── service/                              # Service层
+
+ruoyi-system/src/main/resources/mapper/system/
+└── PosOrderMapper.xml                    # MyBatis XML
+
+ruoyi-admin/src/main/java/com/ruoyi/app/order/
+├── PosOrderShOprateController.java       # 商家操作
+├── PosOrderQsOprateController.java       # 骑手操作
+├── UserOrderController.java              # 用户操作
+├── OrderAppealController.java            # 售后/取消
+└── TestTask.java                         # 定时任务
+
+updatesql/
+└── sql.md                                # SQL迁移脚本
+
+# 前端(外部项目)
+E:\QtwCode\foodie\foodie-admin-vue\
+└── src/views/system/order/index.vue      # 平台管理端订单页
+```
+
+**Structure Decision**: 后端遵循若依多模块 Maven 结构。前端在独立仓库中。
+
+## Complexity Tracking
+
+| Violation | Why Needed | Simpler Alternative Rejected Because |
+|-----------|------------|-------------------------------------|
+| 四字段拆分增加查询复杂度 | 自取/堂食订单无配送状态,单字段无法表达 | 单 state 字段混用已导致无法支持新订单类型 |

+ 51 - 0
specs/006-orderstate/quickstart.md

@@ -0,0 +1,51 @@
+# Quickstart: 订单状态四字段分离
+
+## 前置条件
+
+1. 数据库执行 `updatesql/sql.md` 中的 ALTER TABLE 语句添加三个新列
+2. 历史订单数据手动清空(系统未正式使用)
+
+## 实施顺序
+
+### Step 1: 数据库 & 实体层
+- SQL 脚本写入 `updatesql/sql.md`
+- PosOrder.java 新增 3 个字段
+- PosOrderMapper.xml resultMap + insert/update 添加新字段
+
+### Step 2: 订单创建 & 支付回调
+- UserOrderController.createOrder() 设置新字段初始值
+- 支付回调中更新 payStatus
+
+### Step 3: 商家操作改造
+- 接单:state 0→1
+- 出餐:state 1→2,外送额外设 deliveryStatus=0
+- 完成(自取/堂食):state 2→3 + payStatus 0→1
+
+### Step 4: 骑手操作改造
+- acceptOrder:deliveryStatus=1
+- pickupOrder:deliveryStatus=2
+- deliverOrder:deliveryStatus=3 + state=3
+
+### Step 5: 订单取消
+- OrderAppealController 改造取消逻辑:state→4
+
+### Step 6: 定时任务改造
+- TestTask.java 使用新字段条件
+
+### Step 7: 新列表接口(三端)
+- 用户端 UserOrderController.orderList()
+- 商家端 PosOrderShOprateController.orderList()
+- 骑手端 PosOrderQsOprateController.orderList()
+
+### Step 8: 骑手约束逻辑
+- RiderPositionMapper 条件更新
+
+### Step 9: 平台管理端前端
+- foodie-admin-vue 订单列表页改造
+
+## 验证方式
+
+每个 Step 完成后:
+1. 启动服务确认无编译错误
+2. 用 Postman/curl 测试相关接口
+3. 检查数据库字段值正确

+ 70 - 0
specs/006-orderstate/research.md

@@ -0,0 +1,70 @@
+# Research: 订单状态四字段分离
+
+## 1. 现有状态字段分析
+
+**Decision**: 保留旧 `state` 字段但重新编号,新增 `delivery_status`、`pay_status`、`after_sale_status` 三个字段。
+
+**Rationale**: spec 明确要求参考美团做法。旧 `state` 将 0-13 的支付/订单/配送/退款状态混在一起,自取/堂食订单无法复用。拆分后每种关注点独立,各类型订单只使用相关字段。
+
+**Alternatives considered**:
+- 只加 `type` 判断不改字段:不同类型状态含义不同,查询和展示逻辑会变成大量 if-else,不可维护
+- 保留旧 state 值不变:新值更简洁(0-4 vs 0-13),且旧系统未正式使用无需兼容
+
+## 2. PosOrder 实体现有字段
+
+**Decision**: 新增 `deliveryStatus`(Long)、`payStatus`(Long)、`afterSaleStatus`(Long)三个字段。废弃 `diningStatus`、`isAccepted` 等字段但保留不删。
+
+**Rationale**:
+- 实体已有 `longitude`/`latitude`,骑手距离排序可直接使用
+- `payType` 字段已存在,可用于区分到付/在线支付
+- `qsId`/`qsImg` 字段已存在,骑手端查询可直接用
+
+## 3. MyBatis Mapper 改造策略
+
+**Decision**: 在现有 resultMap 中新增三个字段映射,insert/update 语句同步添加。旧 SQL 查询方法保留不删。
+
+**Rationale**:
+- 旧方法(selectPosOrderList 等)使用旧 state 值,数据迁移后自然废弃
+- 新列表接口使用 LambdaQueryWrapper,不走 XML 定义的条件查询
+- insertPosOrder 和 updatePosOrder 是通用方法,必须包含新字段
+
+## 4. 列表接口技术方案
+
+**Decision**: 三个新列表接口统一使用 MyBatis-Plus `LambdaQueryWrapper`,前端传 `tab` 字符串参数,后端映射到四字段查询条件。
+
+**Rationale**:
+- 项目已集成 MyBatis-Plus(3.0.3 starter),LambdaQueryWrapper 可直接使用
+- Tab 参数用字符串(pending/active/completed 等)语义清晰,与旧接口完全独立
+- 状态展示文字由前端根据字段组合计算,后端只返回数据
+
+**Alternatives considered**:
+- 继续用 XML SQL:旧 SQL 条件用旧 state 值硬编码,改造工作量大且不直观
+- 新建 DTO/VO:PosOrder 全字段序列化已够用,spec 要求前端切换成本低
+
+## 5. 距离排序实现
+
+**Decision**: 骑手端 newTask Tab 使用 `LambdaQueryWrapper.last()` 追加 MySQL `ST_Distance_Sphere` 函数排序。
+
+**Rationale**: PosOrder 已有 longitude/latitude 字段,无需 JOIN PosStore。MySQL 5.7+ 支持 ST_Distance_Sphere。
+
+## 6. 定时任务改造
+
+**Decision**: 改造 TestTask.java 中的四个方法,使用新字段条件。
+
+**Rationale**:
+- `testTiming()`(未支付超时取消):查 `payStatus=0 AND state=0`,设 `state=4`
+- `zidwancheng()`(自动完成):查 `state=2 AND afterSaleStatus=0` 且超时,设 `state=3`
+- `refundProcessing()`:暂不改造(spec 明确本次不实现退款流程)
+- `checkAppointmentOrder()`:条件可能涉及 state,需同步更新
+
+## 7. 前端改造范围
+
+**Decision**: 平台管理端(foodie-admin-vue)的订单列表页需改造筛选条件和状态展示。商家端(foodie-store)和用户端/骑手端为移动 App,不在本次范围内(spec 未提及)。
+
+**Rationale**: spec 第 16 节明确描述了平台管理端的改造细节,是本次需求的明确范围。
+
+## 8. 数据迁移策略
+
+**Decision**: 不需要数据迁移。历史订单数据由开发者手动清空。
+
+**Rationale**: spec 明确"系统未正式使用",新字段通过 ALTER TABLE 添加后所有新订单直接使用新值。旧数据手动清空即可。

+ 228 - 0
specs/006-orderstate/tasks.md

@@ -0,0 +1,228 @@
+# Tasks: 订单状态四字段分离
+
+**Input**: Design documents from `/specs/test/`
+**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/
+
+**Tests**: 无自动化测试要求,通过手动接口测试验证。
+
+**Organization**: 按功能模块(用户故事)分组,每个阶段可独立验证。
+
+## Format: `[ID] [P?] [Story] Description`
+
+- **[P]**: 可并行(不同文件,无依赖)
+- **[Story]**: 所属用户故事(US1-US9)
+- 包含具体文件路径
+
+## Path Conventions
+
+- 后端源码: `ruoyi-system/src/main/java/com/ruoyi/system/`、`ruoyi-admin/src/main/java/com/ruoyi/app/order/`
+- MyBatis XML: `ruoyi-system/src/main/resources/mapper/system/`
+- SQL: `updatesql/sql.md`
+- 前端: `E:\QtwCode\foodie\foodie-admin-vue\src\`
+
+---
+
+## Phase 1: Setup(数据库 & 实体层)
+
+**Purpose**: 添加新字段到数据库和实体类,是所有后续任务的基础。
+
+- [x] T001 在 `updatesql/sql.md` 末尾追加 ALTER TABLE 语句:添加 `delivery_status`(BIGINT DEFAULT NULL)、`pay_status`(BIGINT DEFAULT 0)、`after_sale_status`(BIGINT DEFAULT 0)三列到 pos_order 表
+- [x] T002 [P] 在 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrder.java` 新增三个字段:`deliveryStatus`(Long)、`payStatus`(Long)、`afterSaleStatus`(Long),添加对应 getter/setter
+- [x] T003 在 `ruoyi-system/src/main/resources/mapper/system/PosOrderMapper.xml` 的 resultMap 中添加三个字段映射(delivery_status→deliveryStatus, pay_status→payStatus, after_sale_status→afterSaleStatus)
+- [x] T004 在 `ruoyi-system/src/main/resources/mapper/system/PosOrderMapper.xml` 的 insertPosOrder 语句中添加三个新字段
+- [x] T005 在 `ruoyi-system/src/main/resources/mapper/system/PosOrderMapper.xml` 的 updatePosOrder 语句中添加三个新字段
+
+**Checkpoint**: 数据库 ALTER TABLE 执行完毕,PosOrder 实体和 Mapper 包含新字段,项目编译通过
+
+---
+
+## Phase 2: Foundational(订单创建 & 支付回调)
+
+**Purpose**: 确保新订单创建时正确初始化四字段,支付回调正确更新 payStatus。
+
+- [x] T006 改造 `ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java` 的 `createOrder()` 方法:创建订单时设置 state=0、afterSaleStatus=0、deliveryStatus=NULL;根据类型和支付方式设置 payStatus(外送到付→1,其他→0)
+- [x] T007 [P] 找到支付回调处理代码(搜索 payUrl 或支付回调相关类),在支付成功后添加 `payStatus` 从 0 改为 1 的逻辑
+
+**Checkpoint**: 创建外送/自取/堂食订单后,数据库中四个新字段值正确初始化。模拟支付回调后 payStatus 正确更新为 1。
+
+---
+
+## Phase 3: User Story 1 — 商家操作改造(接单/出餐/完成)
+
+**Goal**: 商家可以对新状态体系的订单执行接单、出餐、完成操作。
+
+**Independent Test**: 创建订单→商家接单(state=1)→商家出餐(state=2, 外送deliveryStatus=0)→自取/堂食完成(state=3, payStatus=1)
+
+### Implementation for User Story 1
+
+- [x] T008 改造 `ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java` 的接单方法:将 state 从 0 改为 1(替换旧逻辑)
+- [x] T009 改造 `ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java` 的 `dispatchOrder()` 出餐方法:将 state 从 1 改为 2;外送订单(type=0)额外设置 deliveryStatus=0;自取/堂食只改 state=2
+- [x] T010 新增完成方法:在 `ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java` 中新增"完成"接口,仅限 type=1(自取)或 type=2(堂食)且 state=2 的订单,将 state 改为 3 同时 payStatus 改为 1
+
+**Checkpoint**: 商家端接单→出餐→完成流程走通,state/deliveryStatus/payStatus 值符合预期
+
+---
+
+## Phase 4: User Story 2 — 骑手操作改造
+
+**Goal**: 骑手可以对已出餐的外送订单执行接单、取餐、送达操作。
+
+**Independent Test**: 商家出餐后(deliveryStatus=0)→骑手接单(deliveryStatus=1)→取餐(deliveryStatus=2)→送达(deliveryStatus=3, state=3)
+
+### Implementation for User Story 2
+
+- [x] T011 改造 `ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderQsOprateController.java` 的 `acceptOrder()`:校验 type=0 且 afterSaleStatus=0,设置 deliveryStatus=1(替换旧 state 逻辑)
+- [x] T012 [P] 改造 `ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderQsOprateController.java` 的 `pickupOrder()`:校验 type=0 且 afterSaleStatus=0,设置 deliveryStatus=2(替换旧 state 逻辑)
+- [x] T013 改造 `ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderQsOprateController.java` 的 `deliverOrder()`:设置 deliveryStatus=3,同时设置 state=3(替换旧 state 逻辑)
+- [x] T014 改造骑手约束逻辑:在骑手接单防重复逻辑中,原来查 `state IN (3,4)` 改为查 `deliveryStatus IN (1,2)`;更新 `ruoyi-system/src/main/java/com/ruoyi/system/mapper/RiderPositionMapper.java` 和对应的 XML 排除条件
+
+**Checkpoint**: 骑手端接单→取餐→送达流程走通,deliveryStatus 和 state 值正确流转
+
+---
+
+## Phase 5: User Story 3 — 订单取消
+
+**Goal**: 用户和商家可以在未接单前取消订单,state 设为 4。
+
+**Independent Test**: 创建订单→用户/商家取消→state=4, afterSaleStatus=0
+
+### Implementation for User Story 3
+
+- [x] T015 改造 `ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderAppealController.java` 的 `userCancelpOrder()`:取消订单时设 state=4, afterSaleStatus 保持 0(替换旧 state=10 逻辑)
+- [x] T016 [P] 在商家端 `ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java` 新增商家取消订单接口:校验 state IN (0,1),设 state=4
+
+**Checkpoint**: 用户取消和商家取消都正确设 state=4
+
+---
+
+## Phase 6: User Story 4 — 定时任务改造
+
+**Goal**: 定时任务使用新字段条件执行超时取消和自动完成。
+
+**Independent Test**: 超时未支付订单自动 state=4;超时未完成订单自动 state=3
+
+### Implementation for User Story 4
+
+- [x] T017 改造 `ruoyi-admin/src/main/java/com/ruoyi/app/order/TestTask.java` 的 `testTiming()`(未支付超时取消):查 payStatus=0 AND state=0 且超时,设 state=4(替换旧 state=0→10 逻辑)
+- [x] T018 改造 `ruoyi-admin/src/main/java/com/ruoyi/app/order/TestTask.java` 的 `zidwancheng()`(自动完成兜底):查 state=2 AND afterSaleStatus=0 且超时,设 state=3(外送同时设 deliveryStatus=3,自取/堂食同时设 payStatus=1)
+- [x] T019 改造 `ruoyi-admin/src/main/java/com/ruoyi/app/order/TestTask.java` 中作废逻辑:原来设 state=10 改为设 state=4
+
+**Checkpoint**: 模拟超时场景,确认定时任务正确更新 state 和关联字段
+
+---
+
+## Phase 7: User Story 5 — 新列表接口(三端)
+
+**Goal**: 用户端、商家端、骑手端各有独立的 Tab 分页列表接口。
+
+**Independent Test**: 各端各 Tab 返回正确的订单列表,分页格式 {records, total, page, size}
+
+### Implementation for User Story 5
+
+- [x] T020 在 `ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java` 新增 `orderList()` 方法:GET /system/userOrder/orderList,支持 tab(unpaid/active/completed/cancelled/refund)、page、size、type 参数,使用 LambdaQueryWrapper 查询,返回 PosOrder 全字段 + 关联数据(shanghu/store/shaddress/user/food/parentRemarks)
+- [x] T021 [P] 在 `ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java` 新增 `orderList()` 方法:GET /system/orderShOprate/orderList,支持 tab(pending/preparing/ready/completed/cancelled/refund)、page、size、mdId、type 参数,使用 LambdaQueryWrapper 查询
+- [x] T022 [P] 在 `ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderQsOprateController.java` 新增 `orderList()` 方法:GET /system/orderQsOprate/orderList,支持 tab(newTask/toPickup/delivering/completed/cancelled/refund)、page、size、longitude、latitude 参数,newTask Tab 使用 ST_Distance_Sphere 距离排序
+
+**Checkpoint**: 三端列表接口各 Tab 返回正确数据,分页 total 准确,骑手端 newTask 距离排序正确
+
+---
+
+## Phase 8: User Story 6 — 平台管理端前端改造
+
+**Goal**: foodie-admin-vue 订单列表页支持四字段筛选和多 Tag 状态展示。
+
+**Independent Test**: 平台管理端可按 state/payStatus/deliveryStatus/afterSaleStatus 筛选,状态列显示多 Tag 组合
+
+### Implementation for User Story 6
+
+- [x] T023 改造 `E:\QtwCode\foodie\foodie-admin-vue\src\views\system\order\index.vue` 筛选区域:将旧 state 单下拉框替换为四个独立下拉框(state/payStatus/deliveryStatus/afterSaleStatus),选项前端硬编码
+- [x] T024 改造 `E:\QtwCode\foodie\foodie-admin-vue\src\views\system\order\index.vue` 状态列:从单个 el-tag 改为主 Tag(state)+ 副 Tag(deliveryStatus/payStatus/afterSaleStatus)组合展示
+- [x] T025 改造 `E:\QtwCode\foodie\foodie-admin-vue\src\views\system\order\index.vue` 订单详情弹窗:进度条从 7 步改为 4 步(0-3),新增支付状态/配送状态/售后状态信息行;修改订单弹窗的 state 下拉改为 0-4
+- [x] T026 改造后端 `/system/order/list` 接口(PosOrderController):支持 state/payStatus/deliveryStatus/afterSaleStatus 查询参数,多个条件 AND 关系,deliveryStatus 支持 NULL 筛选
+
+**Checkpoint**: 平台管理端订单列表页筛选、状态展示、详情弹窗均正常工作
+
+---
+
+## Phase 9: Polish & Cross-Cutting
+
+**Purpose**: 推送通知更新和最终验证。
+
+- [x] T027 更新推送通知代码中的 state 值映射:根据 type + state + deliveryStatus + afterSaleStatus 组合发送不同推送内容
+- [x] T028 [P] 清理旧代码中的注释:在 PosOrder.java 中为废弃字段(diningStatus、isAccepted)添加 @Deprecated 注解
+- [ ] T029 完整流程验证:外送在线支付全流程→外送到付全流程→自取现金全流程→堂食现金全流程→取消订单→定时任务触发
+
+---
+
+## Dependencies & Execution Order
+
+### Phase Dependencies
+
+- **Phase 1 (Setup)**: 无依赖,立即开始
+- **Phase 2 (Foundational)**: 依赖 Phase 1 — BLOCKS 所有后续
+- **Phase 3 (商家操作)**: 依赖 Phase 2
+- **Phase 4 (骑手操作)**: 依赖 Phase 2
+- **Phase 5 (订单取消)**: 依赖 Phase 2
+- **Phase 6 (定时任务)**: 依赖 Phase 2
+- **Phase 7 (列表接口)**: 依赖 Phase 2(可与 Phase 3-6 并行)
+- **Phase 8 (前端改造)**: 依赖 Phase 7(后端接口就绪)
+- **Phase 9 (Polish)**: 依赖所有其他 Phase
+
+### User Story Dependencies
+
+- **US1 (商家操作)**: Phase 2 完成后可开始,独立
+- **US2 (骑手操作)**: Phase 2 完成后可开始,独立,与 US1 并行
+- **US3 (订单取消)**: Phase 2 完成后可开始,独立
+- **US4 (定时任务)**: Phase 2 完成后可开始,独立
+- **US5 (列表接口)**: Phase 2 完成后可开始,独立,与 US1-US4 并行
+- **US6 (前端改造)**: 依赖 US5(后端接口就绪)
+
+### Parallel Opportunities
+
+- Phase 1: T002 可与 T003-T005 并行
+- Phase 3-6: T008-T019 中不同 Phase 的任务可并行
+- Phase 7: T020、T021、T022 可完全并行(三个不同 Controller)
+
+---
+
+## Parallel Example: Phase 7(列表接口三端并行)
+
+```bash
+# 三个列表接口互不依赖,可同时开发:
+Task T020: "用户端 UserOrderController.orderList()"
+Task T021: "商家端 PosOrderShOprateController.orderList()"
+Task T022: "骑手端 PosOrderQsOprateController.orderList()"
+```
+
+---
+
+## Implementation Strategy
+
+### MVP First (Phase 1-3 + Phase 7 用户端)
+
+1. Phase 1: 数据库 & 实体层
+2. Phase 2: 订单创建改造
+3. Phase 3: 商家操作改造
+4. Phase 7: 用户端列表接口
+5. **STOP and VALIDATE**: 商家可以接单→出餐→完成,用户可以查看订单列表
+
+### Full Delivery
+
+1. Phase 1-2 → 基础就绪
+2. + Phase 3 → 商家端可用
+3. + Phase 4 → 骑手端可用
+4. + Phase 5 → 取消功能可用
+5. + Phase 6 → 定时任务就绪
+6. + Phase 7 → 三端列表就绪
+7. + Phase 8 → 平台管理端就绪
+8. + Phase 9 → 完整验证通过
+
+---
+
+## Notes
+
+- [P] 任务 = 不同文件,无依赖,可并行
+- 每个任务完成后确认编译通过
+- Phase 1 完成后需手动执行 SQL(updatesql/sql.md)
+- 前端文件编辑需用 Python 脚本处理 CRLF 换行符问题
+- 旧方法(PosOrderController 中旧列表查询)保留不删