rk
10 小时以前 c9f0611f690b3fb0cb120f5b6799f94977d3f129
server/services/src/main/java/com/doumee/service/business/impl/DriverInfoServiceImpl.java
@@ -17,19 +17,33 @@
import com.doumee.dao.business.MultifileMapper;
import com.doumee.dao.business.SmsrecordMapper;
import com.doumee.dao.business.CategoryMapper;
import com.doumee.dao.business.OrdersMapper;
import com.doumee.dao.business.OrdersDetailMapper;
import com.doumee.dao.business.RevenueMapper;
import com.doumee.biz.system.SystemDictDataBiz;
import com.doumee.biz.system.OperationConfigBiz;
import com.doumee.dao.business.OrderLogMapper;
import com.doumee.dao.business.model.Category;
import com.doumee.dao.business.model.DriverInfo;
import com.doumee.dao.business.model.OrderLog;
import com.doumee.dao.business.model.Member;
import com.doumee.dao.business.model.Multifile;
import com.doumee.dao.business.model.Smsrecord;
import com.doumee.dao.business.model.Orders;
import com.doumee.dao.business.model.OrdersDetail;
import com.doumee.dao.business.model.Revenue;
import com.doumee.dao.vo.AccountResponse;
import com.doumee.dao.vo.DriverCenterVO;
import com.doumee.dao.vo.DriverGrabOrderVO;
import com.doumee.dao.vo.DriverOrderDetailVO;
import com.doumee.dao.dto.AuditDTO;
import com.doumee.dao.dto.ChangeStatusDTO;
import com.doumee.dao.dto.DriverLoginRequest;
import com.doumee.dao.dto.DriverRegisterRequest;
import com.doumee.dao.dto.DriverVerifyRequest;
import com.doumee.service.business.AliSmsService;
import com.doumee.dao.dto.DriverActiveOrderDTO;
import com.doumee.dao.dto.DriverGrabOrderDTO;
import com.doumee.core.utils.aliyun.AliSmsService;
import com.doumee.service.business.DriverInfoService;
import com.alibaba.fastjson.JSONObject;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
@@ -41,10 +55,8 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.*;
import java.util.stream.Collectors;
/**
 * 司机注册信息Service实现
@@ -79,7 +91,22 @@
    private CategoryMapper categoryMapper;
    @Autowired
    private OrdersMapper ordersMapper;
    @Autowired
    private RevenueMapper revenueMapper;
    @Autowired
    private OrdersDetailMapper ordersDetailMapper;
    @Autowired
    private SystemDictDataBiz systemDictDataBiz;
    @Autowired
    private OrderLogMapper orderLogMapper;
    @Autowired
    private OperationConfigBiz operationConfigBiz;
    @Override
    public Integer create(DriverInfo driverInfo) {
@@ -289,7 +316,7 @@
            member.setCreateTime(now);
            member.setUpdateTime(now);
            member.setTelephone(telephone);
            member.setNickName(telephone);
            member.setNickName(telephone.substring(0, 3) + "****" + telephone.substring(7));
            member.setName(telephone);
            member.setUserType(Constants.ONE);
            member.setBusinessStatus(Constants.ZERO);
@@ -555,8 +582,8 @@
        // 拼接图片前缀
        String imgPrefix = "";
        try {
            imgPrefix = systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.RESOURCE_PATH).getCode()
                    + systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.DRIVER_FILES).getCode();
            imgPrefix = systemDictDataBiz.queryByCode(Constants.OSS, Constants.RESOURCE_PATH).getCode()
                    + systemDictDataBiz.queryByCode(Constants.OSS, Constants.DRIVER_FILES).getCode();
        } catch (Exception e) {
            // 未配置时忽略
        }
@@ -630,4 +657,612 @@
        }
    }
    @Override
    public void updateAcceptingStatus(Integer memberId, Integer status) {
        if (!Constants.ZERO.equals(status) && !Constants.ONE.equals(status)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST);
        }
        DriverInfo driver = driverInfoMapper.selectOne(new QueryWrapper<DriverInfo>().lambda()
                .eq(DriverInfo::getMemberId, memberId)
                .eq(DriverInfo::getDeleted, Constants.ZERO)
                .last("limit 1"));
        if (driver == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "司机信息不存在");
        }
        if (!Constants.THREE.equals(driver.getAuditStatus())) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "请先完成押金支付");
        }
        driverInfoMapper.update(new UpdateWrapper<DriverInfo>().lambda()
                .set(DriverInfo::getAcceptingStatus, status)
                .eq(DriverInfo::getId, driver.getId()));
    }
    @Override
    public void updateLocation(Integer memberId, Double longitude, Double latitude) {
        if (longitude == null || latitude == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "经纬度不能为空");
        }
        DriverInfo driver = driverInfoMapper.selectOne(new QueryWrapper<DriverInfo>().lambda()
                .eq(DriverInfo::getMemberId, memberId)
                .eq(DriverInfo::getDeleted, Constants.ZERO)
                .last("limit 1"));
        if (driver == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "司机信息不存在");
        }
        driverInfoMapper.update(new UpdateWrapper<DriverInfo>().lambda()
                .set(DriverInfo::getLongitude, longitude)
                .set(DriverInfo::getLatitude, latitude)
                .eq(DriverInfo::getId, driver.getId()));
    }
    @Override
    public DriverCenterVO getDriverCenterInfo(Integer memberId) {
        DriverInfo driver = driverInfoMapper.selectOne(new QueryWrapper<DriverInfo>().lambda()
                .eq(DriverInfo::getMemberId, memberId)
                .eq(DriverInfo::getDeleted, Constants.ZERO)
                .last("limit 1"));
        if (driver == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "司机信息不存在");
        }
        DriverCenterVO vo = new DriverCenterVO();
        vo.setName(driver.getName());
        vo.setImgUrl(driver.getImgurl());
        vo.setCarCode(driver.getCarCode());
        vo.setScore(driver.getScore() != null ? driver.getScore().toPlainString() : "0");
        vo.setBalance(driver.getBalance() != null ? driver.getBalance() : 0L);
        // 头像全路径
        if (StringUtils.isNotBlank(driver.getImgurl())) {
            String imgPrefix = systemDictDataBiz.queryByCode(Constants.OSS, Constants.RESOURCE_PATH).getCode()
                    + systemDictDataBiz.queryByCode(Constants.OSS, Constants.MEMBER_FILES).getCode();
            vo.setFullImgUrl(imgPrefix + driver.getImgurl());
        }
        // 今日预计佣金:revenue表中今天的收入记录金额之和
        Date now = new Date();
        Calendar cal = Calendar.getInstance();
        cal.setTime(now);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        Date todayStart = cal.getTime();
        QueryWrapper<Revenue> revenueWrapper = new QueryWrapper<>();
        revenueWrapper.lambda()
                .eq(Revenue::getMemberId, memberId)
                .eq(Revenue::getMemberType, Constants.ONE)
                .eq(Revenue::getOptType, Constants.ONE)
                .eq(Revenue::getDeleted, Constants.ZERO)
                .ge(Revenue::getCreateTime, todayStart);
        revenueWrapper.select("IFNULL(SUM(AMOUNT),0) as amount");
        Revenue sumResult = revenueMapper.selectOne(revenueWrapper);
        vo.setTodayCommission(sumResult != null && sumResult.getAmount() != null ? sumResult.getAmount() : 0L);
        // 今日接单数:今天完成的订单数(acceptDriver=司机主键,状态=已完成)
        Long todayOrderCount = ordersMapper.selectCount(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getAcceptDriver, driver.getId())
                .eq(Orders::getDeleted, Constants.ZERO)
                .ge(Orders::getFinishTime, todayStart));
        vo.setTodayOrderCount(todayOrderCount.intValue());
        // 待取货(已接单=3)
        Long waitPickCount = ordersMapper.selectCount(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getAcceptDriver, driver.getId())
                .eq(Orders::getDeleted, Constants.ZERO)
                .eq(Orders::getStatus, Constants.OrderStatus.accepted.getStatus()));
        vo.setWaitPickCount(waitPickCount.intValue());
        // 待配送(派送中=4)
        Long waitDeliverCount = ordersMapper.selectCount(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getAcceptDriver, driver.getId())
                .eq(Orders::getDeleted, Constants.ZERO)
                .eq(Orders::getStatus, Constants.OrderStatus.delivering.getStatus()));
        vo.setWaitDeliverCount(waitDeliverCount.intValue());
        return vo;
    }
    @Override
    public PageData<DriverGrabOrderVO> grabOrderHall(Integer memberId, PageWrap<DriverGrabOrderDTO> pageWrap) {
        DriverGrabOrderDTO dto = pageWrap.getModel();
        // 1. 获取司机定位
        DriverInfo driver = driverInfoMapper.selectOne(new QueryWrapper<DriverInfo>().lambda()
                .eq(DriverInfo::getMemberId, memberId)
                .eq(DriverInfo::getDeleted, Constants.ZERO)
                .last("limit 1"));
        if (driver == null || driver.getLatitude() == null || driver.getLongitude() == null) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "司机位置信息缺失,请先开启定位");
        }
        // 校验司机已支付押金
        if (!Constants.THREE.equals(driver.getAuditStatus())) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "请先完成押金支付");
        }
        double driverLat = driver.getLatitude();
        double driverLng = driver.getLongitude();
        // 2. 预查物品等级对应的物品分类ID
        List<Integer> goodTypeIds = null;
        if (dto != null && dto.getGradeId() != null) {
            List<Category> cats = categoryMapper.selectList(new QueryWrapper<Category>().lambda()
                    .eq(Category::getDeleted, Constants.ZERO)
                    .eq(Category::getType, Constants.TWO)
                    .eq(Category::getRelationId, dto.getGradeId()));
            if (cats.isEmpty()) {
                return emptyPage(pageWrap);
            }
            goodTypeIds = cats.stream().map(Category::getId).collect(Collectors.toList());
        }
        // 3. Haversine SQL公式:司机到存件门店距离(km),使用Orders自带坐标
        String depositDist = "(6371 * acos(cos(radians(" + driverLat + ")) * cos(radians(t.DEPOSIT_LGT)) "
                + "* cos(radians(t.DEPOSIT_LAT) - radians(" + driverLng + ")) "
                + "+ sin(radians(" + driverLat + ")) * sin(radians(t.DEPOSIT_LGT))))";
        // 4. 构造MPJ查询
        IPage<Orders> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        MPJLambdaWrapper<Orders> wrapper = new MPJLambdaWrapper<>();
        wrapper.selectAll(Orders.class)
                // 存件门店
                .select("s1.name", Orders::getDepositShopName)
                .select("s1.address", Orders::getDepositShopAddress)
                // 取件门店
                .select("s2.name", Orders::getTakeShopName)
                .select("s2.address", Orders::getTakeShopAddress)
                .select("s2.link_phone as takeShopLinkPhone")
                // 物品等级贵重标识
                .select("c2.other_field as c2OtherField")
                // JOIN
                .leftJoin("shop_info s1 on s1.id = t.DEPOSIT_SHOP_ID and s1.DELETED = 0")
                .leftJoin("shop_info s2 on s2.id = t.TAKE_SHOP_ID and s2.DELETED = 0")
                .leftJoin("category c1 on c1.id = t.GOOD_TYPE and c1.DELETED = 0")
                .leftJoin("category c2 on c2.id = c1.RELATION_ID and c2.DELETED = 0 and c2.TYPE = 3");
        // 核心条件
        wrapper.eq(Orders::getType, Constants.ONE)
                .eq(Orders::getStatus, Constants.TWO)
                .eq(Orders::getDeleted, Constants.ZERO);
        // 司机级别 >= 订单物品级别
        if (driver.getDriverLevel() != null) {
            wrapper.apply("t.GOOD_LEVEL <= {0}", driver.getDriverLevel());
        }
        // 加急 OR 在配送范围内
        wrapper.and(w -> w
                .eq(Orders::getIsUrgent, Constants.ONE)
                .or()
                .apply(depositDist + " <= s1.delivery_area"));
        // 用户距离过滤
        if (dto != null && dto.getDistance() != null && dto.getDistance() > 0) {
            double maxKm = dto.getDistance() / 1000.0;
            wrapper.apply(depositDist + " <= {0}", maxKm);
        }
        // 物品等级过滤
        if (goodTypeIds != null && !goodTypeIds.isEmpty()) {
            wrapper.in(Orders::getGoodType, goodTypeIds);
        }
        // 排序
        Integer sortType = (dto != null) ? dto.getSortType() : null;
        if (sortType != null && sortType == Constants.TWO) {
            wrapper.last("ORDER BY " + depositDist + " ASC");
        } else {
            wrapper.orderByDesc(Orders::getCreateTime);
        }
        IPage<Orders> result = ordersMapper.selectJoinPage(page, Orders.class, wrapper);
        // 5. 批量查询物品明细
        List<Integer> orderIds = result.getRecords().stream()
                .map(Orders::getId).collect(Collectors.toList());
        Map<Integer, List<OrdersDetail>> detailMap = new HashMap<>();
        if (!orderIds.isEmpty()) {
            List<OrdersDetail> allDetails = ordersDetailMapper.selectList(
                    new QueryWrapper<OrdersDetail>().lambda()
                            .in(OrdersDetail::getOrderId, orderIds));
            for (OrdersDetail d : allDetails) {
                detailMap.computeIfAbsent(d.getOrderId(), k -> new ArrayList<>()).add(d);
            }
        }
        // 6. 构建VO(使用共用方法)
        Date now = new Date();
        List<DriverGrabOrderVO> voList = new ArrayList<>();
        for (Orders order : result.getRecords()) {
            voList.add(buildDriverOrderVO(order, driverLat, driverLng, true, now, detailMap));
        }
        // 7. 手动分页
        PageData<DriverGrabOrderVO> pageData = new PageData<>(result.getCurrent(), result.getSize());
        pageData.setTotal(result.getTotal());
        pageData.setRecords(voList);
        return pageData;
    }
    @Override
    public List<DriverGrabOrderVO> activeOrders(Integer memberId, DriverActiveOrderDTO dto) {
        if (dto == null || dto.getStatus() == null ||
                (!Constants.equalsInteger(dto.getStatus(), Constants.THREE) && !Constants.equalsInteger(dto.getStatus(), Constants.FOUR))) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "订单状态只能为3(已抢单)或4(派送中)");
        }
        // 获取司机信息
        DriverInfo driver = driverInfoMapper.selectOne(new QueryWrapper<DriverInfo>().lambda()
                .eq(DriverInfo::getMemberId, memberId)
                .eq(DriverInfo::getDeleted, Constants.ZERO)
                .last("limit 1"));
        if (driver == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "司机信息不存在");
        }
        boolean needDepositDist = Constants.equalsInteger(dto.getStatus(), Constants.THREE);
        Double driverLat = driver.getLatitude();
        Double driverLng = driver.getLongitude();
        // MPJ查询
        MPJLambdaWrapper<Orders> wrapper = new MPJLambdaWrapper<>();
        wrapper.selectAll(Orders.class)
                .select("s1.name", Orders::getDepositShopName)
                .select("s1.address", Orders::getDepositShopAddress)
                .select("s2.name", Orders::getTakeShopName)
                .select("s2.address", Orders::getTakeShopAddress)
                .select("s2.link_phone as takeShopLinkPhone")
                .select("c2.other_field as c2OtherField")
                .leftJoin("shop_info s1 on s1.id = t.DEPOSIT_SHOP_ID and s1.DELETED = 0")
                .leftJoin("shop_info s2 on s2.id = t.TAKE_SHOP_ID and s2.DELETED = 0")
                .leftJoin("category c1 on c1.id = t.GOOD_TYPE and c1.DELETED = 0")
                .leftJoin("category c2 on c2.id = c1.RELATION_ID and c2.DELETED = 0 and c2.TYPE = 3")
                .eq(Orders::getAcceptDriver, driver.getId())
                .eq(Orders::getType, Constants.ONE)
                .eq(Orders::getStatus, dto.getStatus())
                .eq(Orders::getDeleted, Constants.ZERO)
                .orderByAsc(Orders::getAcceptTime);
        List<Orders> ordersList = ordersMapper.selectJoinList(Orders.class, wrapper);
        // 批量查物品明细
        List<Integer> orderIds = ordersList.stream().map(Orders::getId).collect(Collectors.toList());
        Map<Integer, List<OrdersDetail>> detailMap = new HashMap<>();
        if (!orderIds.isEmpty()) {
            List<OrdersDetail> allDetails = ordersDetailMapper.selectList(
                    new QueryWrapper<OrdersDetail>().lambda()
                            .in(OrdersDetail::getOrderId, orderIds));
            for (OrdersDetail d : allDetails) {
                detailMap.computeIfAbsent(d.getOrderId(), k -> new ArrayList<>()).add(d);
            }
        }
        // 构建VO(使用共用方法)
        Date now = new Date();
        List<DriverGrabOrderVO> voList = new ArrayList<>();
        for (Orders order : ordersList) {
            voList.add(buildDriverOrderVO(order, driverLat, driverLng, needDepositDist, now, detailMap));
        }
        return voList;
    }
    @Override
    public DriverOrderDetailVO driverOrderDetail(Integer driverId, Integer orderId) {
        // 查询订单(MPJ JOIN 门店名称+分类,距离计算使用Orders自带坐标)
        MPJLambdaWrapper<Orders> wrapper = new MPJLambdaWrapper<>();
        wrapper.selectAll(Orders.class)
                .select("s1.name", Orders::getDepositShopName)
                .select("s1.address", Orders::getDepositShopAddress)
                .select("s2.name", Orders::getTakeShopName)
                .select("s2.address", Orders::getTakeShopAddress)
                .select("s2.link_phone as takeShopLinkPhone")
                .select("c2.other_field as c2OtherField")
                .leftJoin("shop_info s1 on s1.id = t.DEPOSIT_SHOP_ID and s1.DELETED = 0")
                .leftJoin("shop_info s2 on s2.id = t.TAKE_SHOP_ID and s2.DELETED = 0")
                .leftJoin("category c1 on c1.id = t.GOOD_TYPE and c1.DELETED = 0")
                .leftJoin("category c2 on c2.id = c1.RELATION_ID and c2.DELETED = 0 and c2.TYPE = 3")
                .eq(Orders::getId, orderId)
                .eq(Orders::getDeleted, Constants.ZERO);
        Orders order = ordersMapper.selectJoinOne(Orders.class, wrapper);
        if (order == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在");
        }
        // 权限校验:抢单大厅可见(status=2, type=1) 或 已抢单/派送中(acceptDriver=driverId)
        boolean canView = false;
        if (Constants.equalsInteger(order.getStatus(), Constants.TWO) && Constants.equalsInteger(order.getType(), Constants.ONE)) {
            canView = true;
        }
        if (driverId != null && driverId.equals(order.getAcceptDriver())) {
            canView = true;
        }
        if (!canView) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "无权查看该订单");
        }
        // 获取司机位置(用于距离计算)
        Double driverLat = null;
        Double driverLng = null;
        if (driverId != null) {
            DriverInfo driver = driverInfoMapper.selectById(driverId);
            if (driver != null) {
                driverLat = driver.getLatitude();
                driverLng = driver.getLongitude();
            }
        }
        // 物品明细
        List<OrdersDetail> details = ordersDetailMapper.selectList(
                new QueryWrapper<OrdersDetail>().lambda()
                        .eq(OrdersDetail::getOrderId, orderId));
        Map<Integer, List<OrdersDetail>> detailMap = new HashMap<>();
        for (OrdersDetail d : details) {
            detailMap.computeIfAbsent(d.getOrderId(), k -> new ArrayList<>()).add(d);
        }
        // 使用共用方法构建基础VO字段
        Date now = new Date();
        DriverGrabOrderVO base = buildDriverOrderVO(order, driverLat, driverLng, true, now, detailMap);
        // 构建详情VO
        DriverOrderDetailVO vo = new DriverOrderDetailVO();
        vo.setId(base.getId());
        vo.setCode(base.getCode());
        vo.setRemainMinutes(base.getRemainMinutes());
        vo.setIsUrgent(base.getIsUrgent());
        vo.setDepositShopName(base.getDepositShopName());
        vo.setDepositShopAddress(base.getDepositShopAddress());
        vo.setDepositDistance(base.getDepositDistance());
        vo.setTakeName(base.getTakeName());
        vo.setTakeDistance(base.getTakeDistance());
        vo.setContactPhone(base.getContactPhone());
        vo.setDriverFee(base.getDriverFee());
        vo.setUrgentAmount(base.getUrgentAmount());
        vo.setIsValuable(base.getIsValuable());
        // 物品明细(转换类型)
        List<DriverOrderDetailVO.OrderItem> detailItems = new ArrayList<>();
        if (base.getItems() != null) {
            for (DriverGrabOrderVO.OrderItem src : base.getItems()) {
                DriverOrderDetailVO.OrderItem item = new DriverOrderDetailVO.OrderItem();
                item.setName(src.getName());
                item.setQuantity(src.getQuantity());
                detailItems.add(item);
            }
        }
        vo.setItems(detailItems);
        // 详情特有字段
        vo.setStatus(order.getStatus());
        vo.setStatusDesc(getStatusDesc(order.getStatus()));
        // 客户信息
        String customerInfo = "";
        if (StringUtils.isNotBlank(order.getTakeUser())) {
            customerInfo = order.getTakeUser();
        }
        if (StringUtils.isNotBlank(order.getTakePhone()) && order.getTakePhone().length() >= 4) {
            customerInfo += "(手机尾号" + order.getTakePhone().substring(order.getTakePhone().length() - 4) + ")";
        }
        vo.setCustomerInfo(customerInfo);
        // 导航经纬度(使用Orders自带坐标)
        if (Constants.equalsInteger(order.getStatus(), Constants.TWO)) {
            if (order.getDepositLgt() != null && order.getDepositLat() != null) {
                vo.setNavigateLat(order.getDepositLgt().doubleValue());
                vo.setNavigateLng(order.getDepositLat().doubleValue());
            }
        } else {
            if (order.getTakeLgt() != null && order.getTakeLat() != null) {
                vo.setNavigateLat(order.getTakeLgt().doubleValue());
                vo.setNavigateLng(order.getTakeLat().doubleValue());
            }
        }
        // 下单附件图片
        String imgPrefix = systemDictDataBiz.queryByCode(Constants.OSS, Constants.RESOURCE_PATH).getCode()
                + systemDictDataBiz.queryByCode(Constants.OSS, Constants.MEMBER_FILES).getCode();
        vo.setOrderImages(getFileUrls(orderId, Constants.FileType.ORDER_FILE.getKey(), imgPrefix));
        return vo;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancelOrder(Integer driverId, Integer orderId, String reason) {
        // 1. 查询司机信息
        DriverInfo driver = driverInfoMapper.selectById(driverId);
        if (driver == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "司机信息不存在");
        }
        // 2. 校验订单
        Orders order = ordersMapper.selectById(orderId);
        if (order == null || Constants.ONE.equals(order.getDeleted())) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在");
        }
        if (!Constants.ONE.equals(order.getType())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅异地寄存订单可取消");
        }
        if (!Constants.TWO.equals(order.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许取消");
        }
        if (!driverId.equals(order.getAcceptDriver())) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "无权操作该订单");
        }
        // 3. 每日取消次数限制
        String limitStr = operationConfigBiz.getConfig().getDriverDailyCancelLimit();
        int limit = 3;
        if (StringUtils.isNotBlank(limitStr)) {
            try { limit = Integer.parseInt(limitStr); } catch (NumberFormatException ignored) {}
        }
        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        Date todayStart = cal.getTime();
        Long todayCancelCount = orderLogMapper.selectCount(new QueryWrapper<OrderLog>().lambda()
                .eq(OrderLog::getOptUserId, driver.getMemberId())
                .eq(OrderLog::getObjType, Constants.ORDER_LOG_CANCEL)
                .eq(OrderLog::getOptUserType, Constants.ONE)
                .ge(OrderLog::getCreateTime, todayStart));
        if (todayCancelCount != null && todayCancelCount >= limit) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "今日取消次数已达上限");
        }
        // 4. 重置订单司机字段(保持status=2,释放回抢单大厅)
        ordersMapper.update(new UpdateWrapper<Orders>().lambda()
                .set(Orders::getAcceptDriver, null)
                .set(Orders::getAcceptTime, null)
                .set(Orders::getAcceptType, null)
                .eq(Orders::getId, orderId));
        // 5. 写入取消日志
        OrderLog log = new OrderLog();
        log.setOrderId(orderId);
        log.setTitle("司机取消订单");
        log.setLogInfo(StringUtils.isNotBlank(reason) ? reason : "司机取消接单");
        log.setObjType(Constants.ORDER_LOG_CANCEL);
        log.setOptUserId(driver.getMemberId());
        log.setOptUserType(Constants.ONE);
        log.setOrderStatus(order.getStatus());
        log.setCreateTime(new Date());
        log.setDeleted(Constants.ZERO);
        orderLogMapper.insert(log);
    }
    private List<String> getFileUrls(Integer orderId, int objType, String prefix) {
        List<Multifile> files = multifileMapper.selectList(
                new QueryWrapper<Multifile>().lambda()
                        .eq(Multifile::getObjId, orderId)
                        .eq(Multifile::getObjType, objType)
                        .eq(Multifile::getIsdeleted, Constants.ZERO)
                        .orderByAsc(Multifile::getSortnum));
        List<String> urls = new ArrayList<>();
        if (files != null) {
            for (Multifile f : files) {
                if (StringUtils.isNotBlank(f.getFileurl())) {
                    urls.add(prefix + f.getFileurl());
                }
            }
        }
        return urls;
    }
    private String getStatusDesc(Integer status) {
        if (status == null) return "";
        switch (status) {
            case 0: return "待支付";
            case 1: return "待寄存";
            case 2: return "已寄存";
            case 3: return "已接单";
            case 4: return "派送中";
            case 5: return "待取件";
            case 7: return "已完成";
            case 96: return "订单关闭";
            case 98: return "取消中";
            case 99: return "已取消";
            default: return "";
        }
    }
    private String formatDistance(double km) {
        if (km < 1) {
            return Math.round(km * 1000) + "m";
        }
        return String.format("%.1fkm", km);
    }
    private double haversine(double lat1, double lng1, double lat2, double lng2) {
        double R = 6371;
        double dLat = Math.toRadians(lat2 - lat1);
        double dLng = Math.toRadians(lng2 - lng1);
        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
                + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
                * Math.sin(dLng / 2) * Math.sin(dLng / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return R * c;
    }
    private <T> PageData<T> emptyPage(PageWrap<?> pageWrap) {
        PageData<T> pd = new PageData<>(pageWrap.getPage(), pageWrap.getCapacity());
        pd.setTotal(0);
        pd.setRecords(new ArrayList<>());
        return pd;
    }
    /**
     * 构建司机端订单列表共用VO(使用Orders自带坐标计算距离)
     *
     * @param order          订单实体(MPJ已填充depositShopName等关联字段)
     * @param driverLat      司机纬度
     * @param driverLng      司机经度
     * @param needDepositDist 是否需要计算距存件门店距离
     * @param now            当前时间
     * @param detailMap      订单物品明细Map(orderId → detailList)
     * @return DriverGrabOrderVO
     */
    private DriverGrabOrderVO buildDriverOrderVO(Orders order, Double driverLat, Double driverLng,
            boolean needDepositDist, Date now, Map<Integer, List<OrdersDetail>> detailMap) {
        DriverGrabOrderVO vo = new DriverGrabOrderVO();
        vo.setId(order.getId());
        vo.setCode(order.getCode());
        // 剩余时长
        if (order.getEstimatedDeliveryTime() != null) {
            long diffMs = order.getEstimatedDeliveryTime().getTime() - now.getTime();
            vo.setRemainMinutes(diffMs > 0 ? diffMs / (60 * 1000) : 0L);
        } else {
            vo.setRemainMinutes(0L);
        }
        vo.setIsUrgent(order.getIsUrgent());
        vo.setDriverFee(order.getDriverFee());
        vo.setUrgentAmount(order.getUrgentAmount());
        // 存件门店(使用Orders自带坐标)
        vo.setDepositShopName(order.getDepositShopName());
        vo.setDepositShopAddress(order.getDepositShopAddress());
        if (needDepositDist && driverLat != null && driverLng != null
                && order.getDepositLgt() != null && order.getDepositLat() != null) {
            double distKm = haversine(driverLat, driverLng,
                    order.getDepositLgt().doubleValue(), order.getDepositLat().doubleValue());
            vo.setDepositDistance(formatDistance(distKm));
        }
        // 取件信息 + 联系电话(使用Orders自带坐标)
        boolean hasTakeShop = order.getTakeShopId() != null && StringUtils.isNotBlank(order.getTakeShopName());
        if (hasTakeShop) {
            vo.setTakeName(order.getTakeShopName());
            vo.setContactPhone(order.getTakeShopLinkPhone());
        } else {
            vo.setTakeName(order.getTakeLocation());
            vo.setContactPhone(order.getTakePhone());
        }
        if (driverLat != null && driverLng != null
                && order.getTakeLgt() != null && order.getTakeLat() != null) {
            double takeDist = haversine(driverLat, driverLng,
                    order.getTakeLgt().doubleValue(), order.getTakeLat().doubleValue());
            vo.setTakeDistance(formatDistance(takeDist));
        }
        // 贵重物品
        vo.setIsValuable("1".equals(order.getC2OtherField()));
        // 物品明细
        List<OrdersDetail> details = detailMap.getOrDefault(order.getId(), Collections.emptyList());
        List<DriverGrabOrderVO.OrderItem> items = new ArrayList<>();
        for (OrdersDetail detail : details) {
            DriverGrabOrderVO.OrderItem item = new DriverGrabOrderVO.OrderItem();
            item.setName(detail.getLuggageName());
            item.setQuantity(detail.getNum());
            items.add(item);
        }
        vo.setItems(items);
        return vo;
    }
}