rk
13 小时以前 967700806dbe285876ad2879e878af84320bb09b
server/services/src/main/java/com/doumee/service/business/impl/OrdersServiceImpl.java
@@ -8,14 +8,16 @@
import com.doumee.biz.system.OperationConfigBiz;
import com.doumee.biz.system.SystemDictDataBiz;
import com.doumee.config.wx.WxMiniConfig;
import com.doumee.config.wx.WxMiniUtilService;
import com.doumee.config.wx.WxPayProperties;
import com.doumee.config.wx.WxPayV3Service;
import com.wechat.pay.java.service.refund.model.Refund;
import com.doumee.core.constants.Constants;
import com.doumee.core.constants.ResponseStatus;
import com.doumee.core.exception.BusinessException;
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.core.utils.DateUtil;
import com.doumee.core.utils.Tencent.MapUtil;
import com.doumee.core.utils.geocode.MapUtil;
import com.doumee.core.utils.Utils;
import com.doumee.dao.business.*;
import com.doumee.dao.business.model.*;
@@ -30,10 +32,12 @@
import com.doumee.dao.dto.MyOrderDTO;
import com.doumee.dao.dto.OrderItemDTO;
import com.doumee.dao.vo.*;
import com.doumee.service.business.NoticeService;
import com.doumee.service.business.OrderLogService;
import com.doumee.service.business.OrdersService;
import com.doumee.dao.business.model.Notice;
import com.doumee.service.business.AreasService;
import com.github.binarywang.wxpay.bean.request.BaseWxPayRequest;
import com.doumee.service.business.PricingRuleService;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.xiaoymin.knife4j.core.util.CollectionUtils;
@@ -44,17 +48,11 @@
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
 * 寄存订单信息Service实现
@@ -103,14 +101,17 @@
    @Autowired
    private RevenueMapper revenueMapper;
    @Autowired
    private WxMiniUtilService wxMiniUtilService;
    @Autowired
    private SystemUserMapper systemUserMapper;
    @Autowired
    private PricingRuleMapper pricingRuleMapper;
    @Autowired
    private PricingRuleService pricingRuleService;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
@@ -122,6 +123,15 @@
    @Autowired
    private AreasService areasService;
    @Autowired
    private NoticeService noticeService;
    @Autowired
    private WxPayV3Service wxPayV3Service;
    @Autowired
    private WxPayProperties wxPayProperties;
    @Override
    public Integer create(Orders orders) {
@@ -303,9 +313,9 @@
     */
    @Override
    public PriceCalculateVO calculateLocalPrice(CalculateLocalPriceDTO dto) {
        // 天数校验,最少1天
        int days = dto.getEstimatedDepositDays() != null && dto.getEstimatedDepositDays() > 0
                ? dto.getEstimatedDepositDays() : 1;
        // 根据开始和结束时间计算天数,最少1天
        long diffMs = dto.getDepositEndTime().getTime() - dto.getDepositStartTime().getTime();
        int days = (int) Math.max(1, (diffMs / (1000 * 60 * 60 * 24)) + 1);
        // 收集所有物品类型ID
        List<Integer> categoryIds = new ArrayList<>();
@@ -364,9 +374,9 @@
            itemPriceTotal += subtotal;
        }
        // 保价费用:报价金额 × 保价费率(字典 INSURANCE_RATE),元→分
        // 保价费用:报价金额 × 保价费率(字典 INSURANCE_RATE),元→分(保价金额>0时计费)
        long insuranceFeeFen = 0L;
        if (Boolean.TRUE.equals(dto.getInsured()) && dto.getDeclaredAmount() != null) {
        if (dto.getDeclaredAmount() != null && dto.getDeclaredAmount().compareTo(BigDecimal.ZERO) > 0) {
            BigDecimal insuranceFeeYuan = calculateInsuranceFee(dto.getDeclaredAmount());
            insuranceFeeFen = insuranceFeeYuan.multiply(new BigDecimal(100)).longValue();
        }
@@ -408,7 +418,7 @@
        // 1. 调用腾讯地图距离矩阵API计算驾车距离
        String from = dto.getFromLat() + "," + dto.getFromLgt();
        String to = dto.getToLat() + "," + dto.getToLgt();
        JSONObject distanceResult = MapUtil.distanceSingle("driving", from, to);
        JSONObject distanceResult = MapUtil.direction("driving", from, to);
        BigDecimal distance = distanceResult.getBigDecimal("distance");
        // distance 单位为米,转为公里
        BigDecimal distanceKm = distance.divide(new BigDecimal(1000), 2, RoundingMode.HALF_UP);
@@ -504,25 +514,26 @@
            itemPriceTotal += subtotal;
        }
        // 4. 保价费用:报价金额 × 保价费率(字典 INSURANCE_RATE),元→分
        // 4. 保价费用:报价金额 × 保价费率(字典 INSURANCE_RATE),元→分(保价金额>0时计费)
        long insuranceFeeFen = 0L;
        if (Boolean.TRUE.equals(dto.getInsured()) && dto.getDeclaredAmount() != null) {
        if (dto.getDeclaredAmount() != null && dto.getDeclaredAmount().compareTo(BigDecimal.ZERO) > 0) {
            BigDecimal insuranceFeeYuan = calculateInsuranceFee(dto.getDeclaredAmount());
            insuranceFeeFen = insuranceFeeYuan.multiply(new BigDecimal(100)).longValue();
        }
        // 5. 加急费用:物品价格 × 加急系数(字典 URGENT_COEFFICIENT)
        long urgentFeeFen = 0L;
        if (Boolean.TRUE.equals(dto.getUrgent())) {
            String urgentRateStr = systemDictDataBiz.queryByCode(
                    Constants.OPERATION_CONFIG, Constants.OP_URGENT_COEFFICIENT).getCode();
            BigDecimal urgentRate = new BigDecimal(urgentRateStr);
            urgentFeeFen = new BigDecimal(itemPriceTotal).multiply(urgentRate)
                    .setScale(0, RoundingMode.HALF_UP).longValue();
        }
        String urgentRateStr = systemDictDataBiz.queryByCode(
                Constants.OPERATION_CONFIG, Constants.OP_URGENT_COEFFICIENT).getCode();
        BigDecimal urgentRate = new BigDecimal(urgentRateStr);
        urgentFeeFen = new BigDecimal(itemPriceTotal).multiply(urgentRate)
                .setScale(0, RoundingMode.HALF_UP).longValue();
        // 6. 总价格 = 物品价格 + 保价费用 + 加急费用
        long totalPrice = itemPriceTotal + insuranceFeeFen + urgentFeeFen;
        // 6. 总价格 = 物品价格 + 保价费用 + 加急费用(加急时才包含加急费)
        long totalPrice = itemPriceTotal + insuranceFeeFen;
        if (Boolean.TRUE.equals(dto.getUrgent())) {
            totalPrice += urgentFeeFen;
        }
        PriceCalculateVO result = new PriceCalculateVO();
        result.setItemList(itemList);
@@ -531,6 +542,33 @@
        result.setUrgentFee(urgentFeeFen);
        result.setTotalPrice(totalPrice);
        result.setDistance(distanceKm);
        // 7. 预计送达时长:pricing_rule type=2(fieldA=1标速达,fieldA=2极速达)
        List<PricingRule> timeRules = pricingRuleMapper.selectList(new QueryWrapper<PricingRule>().lambda()
                .eq(PricingRule::getDeleted, Constants.ZERO)
                .eq(PricingRule::getType, Constants.TWO)
                .eq(PricingRule::getCityId, dto.getCityId())
                .in(PricingRule::getFieldA, Arrays.asList("1", "2")));
        for (PricingRule tr : timeRules) {
            BigDecimal baseKm = new BigDecimal(tr.getFieldB());
            int baseHours = Integer.parseInt(tr.getFieldC());
            BigDecimal extraKm = new BigDecimal(tr.getFieldD());
            int extraHours = Integer.parseInt(tr.getFieldE());
            int hours;
            if (distanceKm.compareTo(baseKm) <= 0) {
                hours = baseHours;
            } else {
                BigDecimal overDistance = distanceKm.subtract(baseKm);
                int extraCount = overDistance.divide(extraKm, 0, RoundingMode.CEILING).intValue();
                hours = baseHours + extraCount * extraHours;
            }
            if ("1".equals(tr.getFieldA())) {
                result.setStandardHours(hours);
            } else if ("2".equals(tr.getFieldA())) {
                result.setUrgentHours(hours);
            }
        }
        return result;
    }
@@ -633,6 +671,11 @@
                takeLgt = BigDecimal.valueOf(takeShop.getLongitude());
                takeLocationValue = takeShop.getAddress();
            } else if (dto.getTakeLat() != null && dto.getTakeLgt() != null && StringUtils.isNotBlank(dto.getTakeLocation())) {
                // 无取件门店,校验存件点与自选取件点是否在同一城市
                if (!MapUtil.isSameCity(depositShop.getLatitude(), depositShop.getLongitude(),
                        dto.getTakeLat().doubleValue(), dto.getTakeLgt().doubleValue())) {
                    throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "异地寄存订单存取点不在同一城市,如需请选择同城门店");
                }
                takeLat = dto.getTakeLat();
                takeLgt = dto.getTakeLgt();
                takeLocationValue = dto.getTakeLocation();
@@ -647,15 +690,12 @@
        // ========== 3. 计算费用 ==========
        PriceCalculateVO priceResult;
        if (Constants.ZERO.equals(dto.getType())) {
            // 就地寄存:计算天数
            long diffMs = takeTime.getTime() - depositTime.getTime();
            int days = (int) Math.max(1, (diffMs / (1000 * 60 * 60 * 24)) + 1);
            // 就地寄存
            CalculateLocalPriceDTO priceDTO = new CalculateLocalPriceDTO();
            priceDTO.setCityId(dto.getCityId());
            priceDTO.setEstimatedDepositDays(days);
            priceDTO.setDepositStartTime(depositTime);
            priceDTO.setDepositEndTime(takeTime);
            priceDTO.setItems(dto.getItems());
            priceDTO.setInsured(dto.getDeclaredAmount() != null && dto.getDeclaredAmount().compareTo(BigDecimal.ZERO) > 0);
            priceDTO.setDeclaredAmount(dto.getDeclaredAmount());
            priceResult = calculateLocalPrice(priceDTO);
        } else {
@@ -667,7 +707,6 @@
            priceDTO.setToLat(takeLat);
            priceDTO.setToLgt(takeLgt);
            priceDTO.setItems(dto.getItems());
            priceDTO.setInsured(dto.getDeclaredAmount() != null && dto.getDeclaredAmount().compareTo(BigDecimal.ZERO) > 0);
            priceDTO.setDeclaredAmount(dto.getDeclaredAmount());
            priceDTO.setUrgent(Constants.ONE.equals(dto.getIsUrgent()));
            priceResult = calculateRemotePrice(priceDTO);
@@ -736,6 +775,7 @@
        // 物品信息
        orders.setGoodType(dto.getGoodType());
        orders.setGoodLevel(goodTypeCategory.getRelationId());
        // 拼接物品信息:物品类型名称、尺寸名称*数量(数组字符串)
        List<String> goodsParts = new ArrayList<>();
        for (ItemPriceVO itemVO : priceResult.getItemList()) {
@@ -802,7 +842,8 @@
        if (member == null || StringUtils.isBlank(member.getOpenid())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "用户信息异常,无法发起支付");
        }
        PayResponse payResponse = wxPay(orders, member.getOpenid(), Constants.OrdersAttach.STORAGE_ORDER);
        PayResponse payResponse = wxPayV3(orders.getOutTradeNo(), orders.getTotalAmount(), orders.getId(),
                member.getOpenid(), Constants.OrdersAttach.STORAGE_ORDER);
        payResponse.setLockKey(lockKey);
        return payResponse;
    }
@@ -837,7 +878,8 @@
        if (member == null || StringUtils.isBlank(member.getOpenid())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "用户信息异常,无法发起支付");
        }
        return wxPay(orders, member.getOpenid(), Constants.OrdersAttach.STORAGE_ORDER);
        return wxPayV3(orders.getOutTradeNo(), orders.getTotalAmount(), orders.getId(),
                member.getOpenid(), Constants.OrdersAttach.STORAGE_ORDER);
    }
    /**
@@ -872,7 +914,32 @@
        }
    }
    /**
     * 唤起微信支付V3
     *
     * @param outTradeNo   商户订单号
     * @param totalCents   支付金额(分)
     * @param orderId      订单主键
     * @param openid       用户微信openid
     * @param ordersAttach 订单支付类型
     * @return PayResponse 包含微信调起参数和订单主键
     */
    private PayResponse wxPayV3(String outTradeNo, Long totalCents, Integer orderId,
                                String openid, Constants.OrdersAttach ordersAttach) {
        Map<String, String> payParams = wxPayV3Service.createOrder(
                outTradeNo,
                ordersAttach.getName(),
                totalCents != null ? totalCents : 0L,
                openid,
                wxPayProperties.getV3NotifyUrl(),
                ordersAttach.getKey()
        );
        PayResponse payResponse = new PayResponse();
        payResponse.setResponse(payParams);
        payResponse.setOrderId(orderId);
        return payResponse;
    }
@@ -1206,14 +1273,17 @@
        // 寄件门店占比:fieldA=0(企业寄)/1(个人寄)
        int depositFieldA = Constants.equalsInteger(depositShop.getCompanyType(), Constants.ONE) ? Constants.ZERO : Constants.ONE;
        BigDecimal depositShopRata = getRevenueShareRata(cityId, depositFieldA);
        // 取件门店占比:fieldA=2(企业取)/3(个人取)
        int takeFieldA = Constants.equalsInteger(takeShop.getCompanyType(), Constants.ONE) ? Constants.TWO : Constants.THREE;
        BigDecimal takeShopRata = getRevenueShareRata(cityId, takeFieldA);
        // 取件门店占比:无取件门店时比例为0
        BigDecimal takeShopRata = BigDecimal.ZERO;
        if (takeShop != null) {
            int takeFieldA = Constants.equalsInteger(takeShop.getCompanyType(), Constants.ONE) ? Constants.TWO : Constants.THREE;
            takeShopRata = getRevenueShareRata(cityId, takeFieldA);
        }
        // 计算薪酬(分):totalAmount 为分,rata 为比例值(如 0.15 表示 15%)
        long driverFee = new BigDecimal(totalAmount).multiply(driverRata).longValue();
        long depositShopFee = new BigDecimal(totalAmount).multiply(depositShopRata).longValue();
        long takeShopFee = totalAmount - driverFee - depositShopFee;
        long takeShopFee = new BigDecimal(totalAmount).multiply(takeShopRata).longValue();
        orders.setDriverFee(driverFee);
        orders.setDepositShopFee(depositShopFee);
@@ -1630,10 +1700,11 @@
            refund.setCreateTime(now);
            refund.setDeleted(Constants.ZERO);
            // 调用微信退款,全额退款
            String refundCode = wxMiniUtilService.wxRefund(order.getOutTradeNo(), order.getPayAmount(), order.getPayAmount());
            refund.setRefundCode(refundCode);
            refund.setRefundTime(new Date());
            // 调用微信退款V3,全额退款
            Refund refundResult = wxPayV3Service.refund(order.getOutTradeNo(), order.getPayAmount(), order.getPayAmount(),
                    "订单退款", wxPayProperties.getV3RefundNotifyUrl());
            refund.setRefundCode(refundResult.getOutRefundNo());
            refund.setStatus(Constants.ZERO); // 退款中
            ordersRefundMapper.insert(refund);
            order.setStatus(Constants.OrderStatus.cancelled.getStatus());
@@ -1642,6 +1713,9 @@
            ordersMapper.updateById(order);
            saveCancelLog(order, "会员取消订单(待寄存,全额退款)", reason, memberId);
            // 通知会员:退款中
            sendOrderNotice(memberId, Constants.MemberOrderNotify.REFUNDING, orderId,
                    "orderNo", order.getCode());
            return;
        }
@@ -1652,6 +1726,11 @@
            order.setCancelTime(now);
            ordersMapper.updateById(order);
            saveCancelLog(order, "会员申请取消订单(已寄存/已接单)", reason, memberId);
            // 通知存件门店:退款申请
            if (order.getDepositShopId() != null) {
                sendShopNotice(order.getDepositShopId(), Constants.ShopOrderNotify.REFUNDING, orderId,
                        "orderNo", order.getCode());
            }
            return;
        }
@@ -1692,6 +1771,52 @@
        orderLogService.create(log);
    }
    /**
     * 发送订单站内信通知
     */
    private void sendOrderNotice(Integer memberId, Constants.MemberOrderNotify notify, Integer orderId, String... params) {
        Notice notice = new Notice();
        notice.setUserType(0); // 0=会员
        notice.setUserId(memberId);
        notice.setTitle(notify.getTitle());
        notice.setContent(notify.format(params));
        notice.setObjId(orderId);
        notice.setObjType(0); // 0=订单
        notice.setStatus(0);  // 0=未读
        notice.setIsdeleted(Constants.ZERO);
        notice.setCreateDate(new Date());
        noticeService.create(notice);
    }
    /**
     * 发送门店站内信通知
     */
    private void sendShopNotice(Integer shopId, Constants.ShopOrderNotify notify, Integer orderId, String... params) {
        Notice notice = new Notice();
        notice.setUserType(2); // 2=门店
        notice.setUserId(shopId);
        notice.setTitle(notify.getTitle());
        notice.setContent(notify.format(params));
        notice.setObjId(orderId);
        notice.setObjType(0); // 0=订单
        notice.setStatus(0);  // 0=未读
        notice.setIsdeleted(Constants.ZERO);
        notice.setCreateDate(new Date());
        noticeService.create(notice);
    }
    /**
     * 通知存件门店和取件门店(订单完成/评价等)
     */
    private void notifyBothShops(Orders order, Constants.ShopOrderNotify notify, String... params) {
        if (order.getDepositShopId() != null) {
            sendShopNotice(order.getDepositShopId(), notify, order.getId(), params);
        }
        if (order.getTakeShopId() != null) {
            sendShopNotice(order.getTakeShopId(), notify, order.getId(), params);
        }
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void handleStorageOrderPayNotify(String outTradeNo, String wxTradeNo) {
@@ -1714,7 +1839,36 @@
        order.setUpdateTime(now);
        // 生成会员核销码
        order.setMemberVerifyCode(generateVerifyCode());
        // 异地寄存:计算预计送达时间
        if (Constants.ONE.equals(order.getType())
                && order.getDepositLat() != null && order.getDepositLgt() != null
                && order.getTakeLat() != null && order.getTakeLgt() != null) {
            EstimatedDeliveryResultVO deliveryResult = calculateEstimatedDelivery(
                    Integer.valueOf(order.getCityId()),
                    order.getDepositLat().doubleValue(), order.getDepositLgt().doubleValue(),
                    order.getTakeLat().doubleValue(), order.getTakeLgt().doubleValue());
            // isUrgent: 0=标速达; 1=极速达
            BigDecimal hours = Constants.ONE.equals(order.getIsUrgent())
                    ? deliveryResult.getExpressHours()
                    : deliveryResult.getStandardHours();
            if (hours != null) {
                long millis = hours.multiply(new BigDecimal("3600000"))
                        .setScale(0, RoundingMode.HALF_UP).longValue();
                order.setEstimatedDeliveryTime(new Date(now.getTime() + millis));
            }
        }
        ordersMapper.updateById(order);
        // 通知会员:订单待核验
        sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.WAIT_VERIFY, order.getId(),
                "orderNo", order.getCode(),
                "storeCode", order.getMemberVerifyCode());
        // 就地寄存订单:通知存件门店待核验
        if (Constants.ZERO.equals(order.getType()) && order.getDepositShopId() != null) {
            sendShopNotice(order.getDepositShopId(), Constants.ShopOrderNotify.WAIT_VERIFY, order.getId(),
                    "orderNo", order.getCode());
        }
    }
    @Override
@@ -1758,8 +1912,9 @@
        otherOrders.setCreateTime(now);
        otherOrdersMapper.insert(otherOrders);
        // 5. 唤起微信支付
        return wxPayForOtherOrder(otherOrders, member.getOpenid(), Constants.OrdersAttach.OVERDUE_FEE);
        // 5. 唤起微信支付V3
        return wxPayV3(otherOrders.getOutTradeNo(), otherOrders.getPayAccount(), otherOrders.getId(),
                member.getOpenid(), Constants.OrdersAttach.OVERDUE_FEE);
    }
    @Override
@@ -1826,9 +1981,10 @@
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public PayResponse payShopDeposit(Integer shopId) {
    public PayResponse payShopDeposit(Integer memberId) {
        // 1. 查询门店信息
        ShopInfo shopInfo = shopInfoMapper.selectById(shopId);
        ShopInfo shopInfo = shopInfoMapper.selectOne(new QueryWrapper<ShopInfo>().lambda()
                .eq(ShopInfo::getRegionMemberId,memberId));
        if (shopInfo == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "门店不存在");
        }
@@ -1852,14 +2008,15 @@
        otherOrders.setMemberId(shopInfo.getRegionMemberId());
        otherOrders.setPayAccount(shopInfo.getDepositAmount());
        otherOrders.setPayStatus(Constants.ZERO);
        otherOrders.setCode("SD" + new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(now) + shopId);
        otherOrders.setCode("SD" + new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(now) + shopInfo.getId());
        otherOrders.setOutTradeNo(outTradeNo);
        otherOrders.setDeleted(Constants.ZERO);
        otherOrders.setCreateTime(now);
        otherOrdersMapper.insert(otherOrders);
        // 5. 唤起微信支付
        return wxPayForOtherOrder(otherOrders, member.getOpenid(), Constants.OrdersAttach.SHOP_DEPOSIT);
        // 5. 唤起微信支付V3
        return wxPayV3(otherOrders.getOutTradeNo(), otherOrders.getPayAccount(), otherOrders.getId(),
                member.getOpenid(), Constants.OrdersAttach.SHOP_DEPOSIT);
    }
    @Override
@@ -1996,6 +2153,12 @@
                    }
                }
            }
            // 通知相关门店:订单已结算
            notifyBothShops(order, Constants.ShopOrderNotify.SETTLED,
                    "orderNo", order.getCode(),
                    "amount", String.valueOf(Constants.getFormatMoney(
                            order.getTotalAmount() != null ? order.getTotalAmount() : 0L)));
        }
    }
@@ -2092,6 +2255,14 @@
        if (isRemote && order.getAcceptDriver() != null) {
            updateTargetScore(Constants.THREE, order.getAcceptDriver());
        }
        // 通知会员:订单已评价
        sendOrderNotice(memberId, Constants.MemberOrderNotify.EVALUATED, order.getId(),
                "orderNo", order.getCode());
        // 通知存件门店和取件门店:订单已评价
        notifyBothShops(order, Constants.ShopOrderNotify.EVALUATED,
                "orderNo", order.getCode());
    }
    /**
@@ -2194,6 +2365,16 @@
            saveVerifyImages(order.getId(), images, Constants.FileType.ORDER_DEPOSIT.getKey(), shopId);
            // 记录订单日志
            saveShopVerifyLog(order, "门店确认寄存", "门店【" + shopName + "】确认寄存", remark, shopId);
            // 通知会员:门店核销成功
            if (Constants.equalsInteger(order.getType(), Constants.ONE)) {
                // 异地寄存 → 待抢单
                sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.WAIT_GRAB, order.getId(),
                        "orderNo", order.getCode());
            } else {
                // 就地寄存 → 待取件提醒
                sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.WAIT_PICKUP_REMIND, order.getId(),
                        "orderNo", order.getCode());
            }
        } else if (Constants.equalsInteger(status, Constants.OrderStatus.arrived.getStatus())) {
            // 异地寄存 + 无取件门店 → 无法核销(客户自取,无门店操作)
            if (Constants.equalsInteger(order.getType(), Constants.ONE) && order.getTakeShopId() == null) {
@@ -2216,6 +2397,14 @@
            generateRevenueRecords(order.getId());
            // 记录订单日志
            saveShopVerifyLog(order, "门店确认取件", "门店【" + shopName + "】确认取件,订单完成", remark, shopId);
            // 通知会员:订单已完成
            sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.FINISHED, order.getId(),
                    "orderNo", order.getCode());
            // 通知存件门店和取件门店:订单已完成
            String settleDays = operationConfigBiz.getConfig().getSettlementDate();
            notifyBothShops(order, Constants.ShopOrderNotify.FINISHED,
                    "orderNo", order.getCode(),
                    "settleDays", settleDays != null ? settleDays : "7");
        } else {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许核销");
        }
@@ -2292,13 +2481,14 @@
            refundRecord.setDeleted(Constants.ZERO);
            ordersRefundMapper.insert(refundRecord);
            // 调用微信退款(放在最后,确保前置操作全部成功)
            String refundCode = wxMiniUtilService.wxRefund(
                    order.getOutTradeNo(), order.getPayAmount(), order.getRefundAmount());
            // 调用微信退款V3(放在最后,确保前置操作全部成功)
            Refund refundResult = wxPayV3Service.refund(
                    order.getOutTradeNo(), order.getPayAmount(), order.getRefundAmount(),
                    "订单退款", wxPayProperties.getV3RefundNotifyUrl());
            // 退款成功后回填退款单号和时间
            refundRecord.setRefundCode(refundCode);
            refundRecord.setRefundTime(new Date());
            // 退款成功后回填退款单号,标记退款中
            refundRecord.setRefundCode(refundResult.getOutRefundNo());
            refundRecord.setStatus(Constants.ZERO); // 退款中
            ordersRefundMapper.updateById(refundRecord);
        }
@@ -2312,6 +2502,60 @@
            logInfo += ",退款" + Constants.getFormatMoney(order.getRefundAmount()) + "元";
        }
        saveShopVerifyLog(order, "门店确认出库", logInfo, remark, shopId);
        // 通知会员:订单已完成
        sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.FINISHED, order.getId(),
                "orderNo", order.getCode());
        // 通知存件门店和取件门店:订单已完成
        String settleDays = operationConfigBiz.getConfig().getSettlementDate();
        notifyBothShops(order, Constants.ShopOrderNotify.FINISHED,
                "orderNo", order.getCode(),
                "settleDays", settleDays != null ? settleDays : "7");
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void memberConfirmReceipt(Integer orderId, Integer memberId) {
        // 1. 查询订单
        Orders order = ordersMapper.selectById(orderId);
        if (order == null || Constants.equalsInteger(order.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在");
        }
        // 2. 校验归属
        if (!memberId.equals(order.getMemberId())) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "无权操作该订单");
        }
        // 3. 校验订单类型:异地寄存
        if (!Constants.ONE.equals(order.getType())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅异地寄存订单可操作");
        }
        // 4. 校验无取件门店
        if (order.getTakeShopId() != null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单有取件门店,需门店确认出库");
        }
        // 5. 校验状态:已送达(5)
        if (!Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.arrived.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许确认收货");
        }
        // 6. 更新订单状态为已完成
        Date now = new Date();
        order.setStatus(Constants.OrderStatus.finished.getStatus());
        order.setFinishTime(now);
        order.setUpdateTime(now);
        ordersMapper.updateById(order);
        // 7. 生成收益记录
        calculateAndSaveOrderFees(orderId);
        generateRevenueRecords(orderId);
        // 通知会员:订单已完成
        sendOrderNotice(memberId, Constants.MemberOrderNotify.FINISHED, orderId,
                "orderNo", order.getCode());
        // 通知存件门店和取件门店:订单已完成
        String settleDays = operationConfigBiz.getConfig().getSettlementDate();
        notifyBothShops(order, Constants.ShopOrderNotify.FINISHED,
                "orderNo", order.getCode(),
                "settleDays", settleDays != null ? settleDays : "7");
    }
    @Override
@@ -2463,6 +2707,26 @@
        // 保存附件(obj_type=3 门店入库图片,最多3张)
        saveVerifyImages(order.getId(), images, Constants.FileType.ORDER_TAKE.getKey(), driverId);
        // 通知会员:订单已送达
        String destination = order.getTakeShopAddress() != null ? order.getTakeShopAddress() : "";
        if (order.getMemberVerifyCode() != null) {
            sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.ARRIVED_HAS_SHOP, order.getId(),
                    "orderNo", order.getCode(),
                    "destination", destination,
                    "pickupCode", order.getMemberVerifyCode());
        } else {
            sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.ARRIVED_NO_SHOP, order.getId(),
                    "orderNo", order.getCode(),
                    "destination", destination);
        }
        // 通知取件门店:订单已送达
        if (order.getTakeShopId() != null) {
            sendShopNotice(order.getTakeShopId(), Constants.ShopOrderNotify.ARRIVED, order.getId(),
                    "orderNo", order.getCode(),
                    "destination", destination);
        }
    }
    /**
@@ -2857,4 +3121,29 @@
        return Math.max(days, 0);
    }
    @Override
    public EstimatedDeliveryResultVO calculateEstimatedDelivery(Integer cityId,
                                                                Double fromLat, Double fromLng,
                                                                Double toLat, Double toLng) {
        // 腾讯地图距离矩阵API计算实际距离
        String from = fromLat + "," + fromLng;
        String to = toLat + "," + toLng;
        JSONObject distanceResult = MapUtil.direction("driving", from, to);
        // 获取距离(米),转公里
        int distanceMeters = distanceResult.getIntValue("distance");
        BigDecimal distanceKm = new BigDecimal(distanceMeters)
                .divide(new BigDecimal("1000"), 2, RoundingMode.HALF_UP);
        // 根据pricing_rule type=2 计算 标速达(1) 和 极速达(2) 时效
        BigDecimal standardTime = pricingRuleService.calculateEstimatedTime(cityId, 1, distanceKm);
        BigDecimal expressTime = pricingRuleService.calculateEstimatedTime(cityId, 2, distanceKm);
        EstimatedDeliveryResultVO vo = new EstimatedDeliveryResultVO();
        vo.setDistanceKm(distanceKm);
        vo.setStandardHours(standardTime);
        vo.setExpressHours(expressTime);
        return vo;
    }
}