rk
昨天 ab9cd2c82bd64de8e33510db1d1e78a5b3b4de70
server/services/src/main/java/com/doumee/service/business/impl/OrdersServiceImpl.java
@@ -8,29 +8,36 @@
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.*;
import com.doumee.dao.system.SystemUserMapper;
import com.doumee.dao.system.model.SystemDictData;
import com.doumee.dao.system.model.SystemUser;
import com.doumee.dao.dto.CalculateLocalPriceDTO;
import com.doumee.dao.dto.CalculateRemotePriceDTO;
import com.doumee.dao.dto.CommentOrderDTO;
import com.doumee.dao.dto.CreateOrderDTO;
import com.doumee.dao.dto.DispatchDTO;
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.github.binarywang.wxpay.bean.request.BaseWxPayRequest;
import com.doumee.dao.business.model.Notice;
import com.doumee.service.business.AreasService;
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;
@@ -41,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实现
@@ -92,13 +93,25 @@
    private OrdersRefundMapper ordersRefundMapper;
    @Autowired
    private WxMiniUtilService wxMiniUtilService;
    private OtherOrdersMapper otherOrdersMapper;
    @Autowired
    private OrderCommentMapper orderCommentMapper;
    @Autowired
    private RevenueMapper revenueMapper;
    @Autowired
    private SystemUserMapper systemUserMapper;
    @Autowired
    private PricingRuleMapper pricingRuleMapper;
    @Autowired
    private PricingRuleService pricingRuleService;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
@@ -107,6 +120,18 @@
    @Autowired
    private OperationConfigBiz operationConfigBiz;
    @Autowired
    private AreasService areasService;
    @Autowired
    private NoticeService noticeService;
    @Autowired
    private WxPayV3Service wxPayV3Service;
    @Autowired
    private WxPayProperties wxPayProperties;
    @Override
    public Integer create(Orders orders) {
@@ -288,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<>();
@@ -349,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();
        }
@@ -393,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);
@@ -489,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);
@@ -516,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;
    }
@@ -618,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();
@@ -632,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 {
@@ -652,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);
@@ -721,6 +775,7 @@
        // 物品信息
        orders.setGoodType(dto.getGoodType());
        orders.setGoodLevel(goodTypeCategory.getRelationId());
        // 拼接物品信息:物品类型名称、尺寸名称*数量(数组字符串)
        List<String> goodsParts = new ArrayList<>();
        for (ItemPriceVO itemVO : priceResult.getItemList()) {
@@ -787,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;
    }
@@ -822,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);
    }
    /**
@@ -857,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;
    }
@@ -1135,8 +1217,8 @@
    private String getOrdersPrefix() {
        try {
            return systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.RESOURCE_PATH).getCode()
                    + systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.ORDERS_FILES).getCode();
            return systemDictDataBiz.queryByCode(Constants.OSS, Constants.RESOURCE_PATH).getCode()
                    + systemDictDataBiz.queryByCode(Constants.OSS, Constants.ORDERS_FILES).getCode();
        } catch (Exception e) {
            return "";
        }
@@ -1191,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);
@@ -1307,6 +1392,102 @@
                vo.setDetailList(buildDetailList(details));
                // 逾期信息(仅待取件状态计算)
                if (Integer.valueOf(Constants.OrderStatus.arrived.getStatus()).equals(o.getStatus())) {
                    OverdueFeeVO overdueInfo = calculateOverdueFeeInternal(o, details);
                    vo.setOverdue(overdueInfo.getOverdue());
                    vo.setOverdueDays(overdueInfo.getOverdueDays());
                    vo.setOverdueFee(overdueInfo.getOverdueFee());
                }
                voList.add(vo);
            }
        }
        IPage<MyOrderVO> vPage = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        PageData<MyOrderVO> pageData = PageData.from(vPage);
        pageData.setRecords(voList);
        pageData.setTotal(orderPage.getTotal());
        pageData.setPage(orderPage.getCurrent());
        pageData.setCapacity(orderPage.getSize());
        return pageData;
    }
    @Override
    public PageData<MyOrderVO> findShopOrderPage(PageWrap<MyOrderDTO> pageWrap, Integer shopId) {
        MyOrderDTO model = pageWrap.getModel();
        Integer status = model != null ? model.getStatus() : null;
        Integer combinedStatus = model != null ? model.getCombinedStatus() : null;
        // 解析合并状态为具体状态列表
        List<Integer> statusList = null;
        if (combinedStatus != null) {
            Constants.OrderCombinedStatus combined = Constants.OrderCombinedStatus.getByKey(combinedStatus);
            if (combined != null) {
                statusList = new ArrayList<>();
                for (int s : combined.getStatuses()) {
                    statusList.add(s);
                }
            }
        }
        IPage<Orders> p = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        MPJLambdaWrapper<Orders> wrapper = new MPJLambdaWrapper<Orders>()
                .selectAll(Orders.class)
                .select("s1.name", Orders::getDepositShopName)
                .select("s1.link_name", Orders::getDepositShopLinkName)
                .select("s1.link_phone", Orders::getDepositShopLinkPhone)
                .select("s2.name", Orders::getTakeShopName)
                .select("s2.address", Orders::getTakeShopAddress)
                .leftJoin("shop_info s1 on s1.id = t.DEPOSIT_SHOP_ID")
                .leftJoin("shop_info s2 on s2.id = t.TAKE_SHOP_ID")
                .eq(Orders::getPayStatus, Constants.ONE)
                .and(w -> w.eq(Orders::getDepositShopId, shopId).or().eq(Orders::getTakeShopId, shopId))
                .eq(status != null, Orders::getStatus, status)
                .in(statusList != null, Orders::getStatus, statusList)
                .orderByDesc(Orders::getCreateTime);
        IPage<Orders> orderPage = ordersMapper.selectJoinPage(p, Orders.class, wrapper);
        List<MyOrderVO> voList = new ArrayList<>();
        if (orderPage != null && orderPage.getRecords() != null) {
            for (Orders o : orderPage.getRecords()) {
                MyOrderVO vo = new MyOrderVO();
                vo.setId(o.getId());
                vo.setCode(o.getCode());
                vo.setType(o.getType());
                vo.setStatus(o.getStatus());
                vo.setCreateTime(o.getCreateTime());
                vo.setExpectedTakeTime(o.getExpectedTakeTime());
                vo.setDepositShopName(o.getDepositShopName());
                vo.setDepositShopLinkName(o.getDepositShopLinkName());
                vo.setDepositShopPhone(o.getDepositShopLinkPhone());
                // 门店角色:存件门店=1,取件门店=2
                if (Constants.equalsInteger(o.getDepositShopId(), shopId)) {
                    vo.setShopRole(Constants.ONE);
                } else if (Constants.equalsInteger(o.getTakeShopId(), shopId)) {
                    vo.setShopRole(Constants.TWO);
                }
                if (o.getTakeShopId() != null) {
                    vo.setTakeShopName(o.getTakeShopName());
                    vo.setTakeShopAddress(o.getTakeShopAddress());
                } else {
                    vo.setTakeLocation(o.getTakeLocation());
                    vo.setTakeLocationRemark(o.getTakeLocationRemark());
                }
                vo.setTakeUser(o.getTakeUser());
                vo.setTakePhone(o.getTakePhone());
                vo.setDeclaredFee(o.getDeclaredFee());
                vo.setEstimatedAmount(o.getEstimatedAmount());
                List<OrdersDetail> details = ordersDetailMapper.selectList(
                        new QueryWrapper<OrdersDetail>().lambda()
                                .eq(OrdersDetail::getOrderId, o.getId())
                                .eq(OrdersDetail::getDeleted, Constants.ZERO));
                vo.setDetailList(buildDetailList(details));
                if (Integer.valueOf(Constants.OrderStatus.arrived.getStatus()).equals(o.getStatus())) {
                    OverdueFeeVO overdueInfo = calculateOverdueFeeInternal(o, details);
                    vo.setOverdue(overdueInfo.getOverdue());
@@ -1519,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());
@@ -1531,6 +1713,9 @@
            ordersMapper.updateById(order);
            saveCancelLog(order, "会员取消订单(待寄存,全额退款)", reason, memberId);
            // 通知会员:退款中
            sendOrderNotice(memberId, Constants.MemberOrderNotify.REFUNDING, orderId,
                    "orderNo", order.getCode());
            return;
        }
@@ -1541,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;
        }
@@ -1581,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) {
@@ -1603,7 +1839,489 @@
        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
    @Transactional(rollbackFor = Exception.class)
    public PayResponse payOverdueFee(Integer orderId, Integer memberId) {
        // 1. 查询寄存订单
        Orders order = ordersMapper.selectOne(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getId, orderId)
                .eq(Orders::getMemberId, memberId)
                .eq(Orders::getDeleted, Constants.ZERO));
        if (order == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在");
        }
        // 2. 校验状态:待取件(5) + 逾期(1)
        if (!Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.arrived.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不支持逾期支付");
        }
        if (!Constants.equalsInteger(order.getOverdueStatus(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单不存在逾期费用");
        }
        if (order.getOverdueAmount() == null || order.getOverdueAmount() <= 0) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "逾期费用异常,无法发起支付");
        }
        // 3. 查询会员
        Member member = memberMapper.selectById(memberId);
        if (member == null || StringUtils.isBlank(member.getOpenid())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "用户信息异常,无法发起支付");
        }
        // 4. 创建逾期费用订单
        String outTradeNo = generateOrderTradeNo();
        Date now = new Date();
        OtherOrders otherOrders = new OtherOrders();
        otherOrders.setType(Constants.TWO); // 2=逾期费用订单
        otherOrders.setMemberId(memberId);
        otherOrders.setOrderId(orderId);
        otherOrders.setPayAccount(order.getOverdueAmount());
        otherOrders.setPayStatus(Constants.ZERO);
        otherOrders.setCode("OD" + new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(now) + orderId);
        otherOrders.setOutTradeNo(outTradeNo);
        otherOrders.setDeleted(Constants.ZERO);
        otherOrders.setCreateTime(now);
        otherOrdersMapper.insert(otherOrders);
        // 5. 唤起微信支付V3
        return wxPayV3(otherOrders.getOutTradeNo(), otherOrders.getPayAccount(), otherOrders.getId(),
                member.getOpenid(), Constants.OrdersAttach.OVERDUE_FEE);
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void handleOverdueFeePayNotify(String outTradeNo, String wxTradeNo) {
        // 1. 查找逾期费用订单
        OtherOrders otherOrders = otherOrdersMapper.selectOne(new QueryWrapper<OtherOrders>().lambda()
                .eq(OtherOrders::getOutTradeNo, outTradeNo)
                .eq(OtherOrders::getDeleted, Constants.ZERO)
                .last("limit 1"));
        if (otherOrders == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "逾期费用订单不存在: " + outTradeNo);
        }
        // 2. 幂等:已支付则跳过
        if (Constants.equalsInteger(otherOrders.getPayStatus(), Constants.ONE)) {
            return;
        }
        Date now = new Date();
        // 3. 更新逾期费用订单状态
        otherOrders.setPayStatus(Constants.ONE);
        otherOrders.setPayTime(now);
        otherOrders.setWxExternalNo(wxTradeNo);
        otherOrders.setUpdateTime(now);
        otherOrdersMapper.updateById(otherOrders);
        // 4. 更新寄存订单逾期状态为已支付(2),更新总金额,重算三方收益
        if (otherOrders.getOrderId() != null) {
            Orders order = ordersMapper.selectById(otherOrders.getOrderId());
            if (order != null) {
                order.setOverdueStatus(Constants.TWO); // 2=已支付
                // 总金额 = 原金额 + 逾期费用
                Long overdueFee = otherOrders.getPayAccount() != null ? otherOrders.getPayAccount() : 0L;
                long newTotal = (order.getTotalAmount() != null ? order.getTotalAmount() : 0L) + overdueFee;
                order.setTotalAmount(newTotal);
                order.setUpdateTime(now);
                ordersMapper.updateById(order);
                // 重算三方收益
                calculateAndSaveOrderFees(order.getId());
            }
        }
    }
    @Override
    public void deleteMyOrder(Integer orderId, Integer memberId) {
        Orders order = ordersMapper.selectById(orderId);
        if (order == null || !Constants.equalsInteger(order.getDeleted(), Constants.ZERO)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        if (!Constants.equalsInteger(order.getMemberId(), memberId)) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "无权操作此订单");
        }
        // 仅已完成(7)、已取消(99)、已退款(96)可删除
        int status = Constants.formatIntegerNum(order.getStatus());
        if (status != Constants.OrderStatus.finished.getStatus()
                && status != Constants.OrderStatus.cancelled.getStatus()
                && status != Constants.OrderStatus.closed.getStatus()) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "当前订单状态不可删除");
        }
        ordersMapper.update(new UpdateWrapper<Orders>().lambda()
                .set(Orders::getDeleted, Constants.ONE)
                .set(Orders::getUpdateTime, new Date())
                .eq(Orders::getId, orderId));
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public PayResponse payShopDeposit(Integer shopId) {
        // 1. 查询门店信息
        ShopInfo shopInfo = shopInfoMapper.selectById(shopId);
        if (shopInfo == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "门店不存在");
        }
        // 2. 校验状态:审批通过(1)才能支付押金
        if (!Constants.equalsInteger(shopInfo.getAuditStatus(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前门店状态不支持支付押金");
        }
        if (shopInfo.getDepositAmount() == null || shopInfo.getDepositAmount() <= 0) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "押金金额异常,无法发起支付");
        }
        // 3. 查询会员openid
        Member member = memberMapper.selectById(shopInfo.getRegionMemberId());
        if (member == null || StringUtils.isBlank(member.getOpenid())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "用户信息异常,无法发起支付");
        }
        // 4. 创建押金订单
        String outTradeNo = generateOrderTradeNo();
        Date now = new Date();
        OtherOrders otherOrders = new OtherOrders();
        otherOrders.setType(Constants.ZERO); // 0=店铺押金订单
        otherOrders.setMemberId(shopInfo.getRegionMemberId());
        otherOrders.setPayAccount(shopInfo.getDepositAmount());
        otherOrders.setPayStatus(Constants.ZERO);
        otherOrders.setCode("SD" + new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(now) + shopId);
        otherOrders.setOutTradeNo(outTradeNo);
        otherOrders.setDeleted(Constants.ZERO);
        otherOrders.setCreateTime(now);
        otherOrdersMapper.insert(otherOrders);
        // 5. 唤起微信支付V3
        return wxPayV3(otherOrders.getOutTradeNo(), otherOrders.getPayAccount(), otherOrders.getId(),
                member.getOpenid(), Constants.OrdersAttach.SHOP_DEPOSIT);
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void handleShopDepositPayNotify(String outTradeNo, String wxTradeNo) {
        // 1. 查找押金订单
        OtherOrders otherOrders = otherOrdersMapper.selectOne(new QueryWrapper<OtherOrders>().lambda()
                .eq(OtherOrders::getOutTradeNo, outTradeNo)
                .eq(OtherOrders::getDeleted, Constants.ZERO)
                .last("limit 1"));
        if (otherOrders == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "押金订单不存在: " + outTradeNo);
        }
        // 2. 幂等:已支付则跳过
        if (Constants.equalsInteger(otherOrders.getPayStatus(), Constants.ONE)) {
            return;
        }
        Date now = new Date();
        // 3. 更新押金订单状态
        otherOrders.setPayStatus(Constants.ONE);
        otherOrders.setPayTime(now);
        otherOrders.setWxExternalNo(wxTradeNo);
        otherOrders.setUpdateTime(now);
        otherOrdersMapper.updateById(otherOrders);
        // 4. 查询门店信息(通过注册会员主键关联)
        ShopInfo shopInfo = shopInfoMapper.selectOne(new QueryWrapper<ShopInfo>().lambda()
                .eq(ShopInfo::getRegionMemberId, otherOrders.getMemberId())
                .eq(ShopInfo::getDeleted, Constants.ZERO)
                .last("limit 1"));
        if (shopInfo == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "门店不存在");
        }
        // 5. 更新门店状态:已支付押金
        shopInfo.setAuditStatus(Constants.THREE); // 3=已支付押金
        shopInfo.setPayStatus(Constants.ONE);
        shopInfo.setPayTime(now);
        shopInfo.setWxExternalNo(wxTradeNo);
        shopInfo.setCode(otherOrders.getCode());
        Member member = memberMapper.selectById(otherOrders.getMemberId());
        if (member != null) {
            shopInfo.setPayMemberOpenId(member.getOpenid());
        }
        shopInfo.setUpdateTime(now);
        shopInfoMapper.updateById(shopInfo);
        // 6. 押金支付完成后,若城市未开通则自动开通
        if (shopInfo.getAreaId() != null) {
            Areas shopArea = areasBiz.resolveArea(shopInfo.getAreaId());
            if (shopArea != null && shopArea.getParentId() != null) {
                Areas cityArea = areasBiz.resolveArea(shopArea.getParentId());
                if (cityArea != null && !Constants.equalsInteger(cityArea.getStatus(), Constants.ONE)) {
                    cityArea.setStatus(Constants.ONE);
                    cityArea.setEditDate(now);
                    areasService.updateById(cityArea);
                    areasService.cacheData();
                }
            }
        }
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void settleOrders() {
        // 1. 读取结算天数配置
        SystemDictData settlementConfig = systemDictDataBiz.queryByCode(Constants.OPERATION_CONFIG, Constants.OP_SETTLEMENT_DATE);
        if (settlementConfig == null || StringUtils.isBlank(settlementConfig.getCode())) {
            return;
        }
        int days = Integer.parseInt(settlementConfig.getCode());
        // 结算截止时间 = 当前时间 - N天
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DAY_OF_MONTH, -days);
        Date deadline = cal.getTime();
        // 2. 查询已完成的待结算订单(完成时间 <= 截止时间)
        List<Orders> ordersList = ordersMapper.selectList(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .eq(Orders::getStatus, Constants.OrderStatus.finished.getStatus())
                .eq(Orders::getSettlementStatus, Constants.ZERO)
                .le(Orders::getFinishTime, deadline));
        if (ordersList == null || ordersList.isEmpty()) {
            return;
        }
        Date now = new Date();
        for (Orders order : ordersList) {
            // 3. 更新订单结算状态
            ordersMapper.update(new UpdateWrapper<Orders>().lambda()
                    .set(Orders::getSettlementStatus, Constants.ONE)
                    .set(Orders::getSettlementTime, now)
                    .set(Orders::getUpdateTime, now)
                    .eq(Orders::getId, order.getId()));
            // 4. 查询关联的待入账 Revenue 记录
            List<Revenue> revenues = revenueMapper.selectList(new QueryWrapper<Revenue>().lambda()
                    .eq(Revenue::getObjId, order.getId())
                    .eq(Revenue::getObjType, Constants.ZERO)
                    .eq(Revenue::getVaildStatus, Constants.ZERO)
                    .eq(Revenue::getDeleted, Constants.ZERO));
            for (Revenue revenue : revenues) {
                Long amount = revenue.getAmount() != null ? revenue.getAmount() : 0L;
                // 更新 Revenue 为已入账
                revenueMapper.update(new UpdateWrapper<Revenue>().lambda()
                        .set(Revenue::getVaildStatus, Constants.ONE)
                        .set(Revenue::getUpdateTime, now)
                        .eq(Revenue::getId, revenue.getId()));
                // 根据 memberType 更新余额
                if (Constants.equalsInteger(revenue.getMemberType(), Constants.ONE)) {
                    // 司机:通过 memberId 查 DriverInfo,更新 balance / totalBalance
                    DriverInfo driver = driverInfoMapper.selectOne(new QueryWrapper<DriverInfo>().lambda()
                            .eq(DriverInfo::getMemberId, revenue.getMemberId())
                            .eq(DriverInfo::getDeleted, Constants.ZERO)
                            .last("limit 1"));
                    if (driver != null) {
                        driverInfoMapper.update(new UpdateWrapper<DriverInfo>().lambda()
                                .setSql(" BALANCE = IFNULL(BALANCE, 0) + " + amount)
                                .setSql(" TOTAL_BALANCE = IFNULL(TOTAL_BALANCE, 0) + " + amount)
                                .eq(DriverInfo::getId, driver.getId()));
                    }
                } else if (Constants.equalsInteger(revenue.getMemberType(), Constants.TWO)) {
                    // 门店:通过 memberId 查 ShopInfo(regionMemberId),更新 balance / totalBalance
                    ShopInfo shop = shopInfoMapper.selectOne(new QueryWrapper<ShopInfo>().lambda()
                            .eq(ShopInfo::getRegionMemberId, revenue.getMemberId())
                            .eq(ShopInfo::getDeleted, Constants.ZERO)
                            .last("limit 1"));
                    if (shop != null) {
                        shopInfoMapper.update(new UpdateWrapper<ShopInfo>().lambda()
                                .setSql(" BALANCE = IFNULL(BALANCE, 0) + " + amount)
                                .setSql(" TOTAL_BALANCE = IFNULL(TOTAL_BALANCE, 0) + " + amount)
                                .eq(ShopInfo::getId, shop.getId()));
                    }
                }
            }
            // 通知相关门店:订单已结算
            notifyBothShops(order, Constants.ShopOrderNotify.SETTLED,
                    "orderNo", order.getCode(),
                    "amount", String.valueOf(Constants.getFormatMoney(
                            order.getTotalAmount() != null ? order.getTotalAmount() : 0L)));
        }
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void commentOrder(CommentOrderDTO dto, Integer memberId) {
        // 1. 校验订单
        Orders order = ordersMapper.selectById(dto.getOrderId());
        if (order == null || Constants.equalsInteger(order.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在");
        }
        if (!Constants.equalsInteger(order.getMemberId(), memberId)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "无权评价该订单");
        }
        if (!Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.finished.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不支持评价");
        }
        if (Constants.equalsInteger(order.getCommentStatus(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单已评价");
        }
        // 2. 异地寄存订单:取件门店和司机评分校验
        boolean isRemote = Constants.equalsInteger(order.getType(), Constants.ONE);
        if (isRemote) {
            if (dto.getDriverScore() == null) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "异地寄存订单必须评价司机");
            }
            if (order.getTakeShopId() != null && dto.getTakeScore() == null) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "请评价取件门店");
            }
        }
        Date now = new Date();
        // 3. 更新订单评价状态
        order.setCommentStatus(Constants.ONE);
        order.setCommentInfo(dto.getContent());
        order.setCommentDepositLevel(dto.getDepositScore());
        order.setCommentTakeLevel(dto.getTakeScore());
        order.setCommentDriverLevel(dto.getDriverScore());
        order.setCommentTime(now);
        order.setUpdateTime(now);
        ordersMapper.updateById(order);
        // 4. 创建评价记录
        // 4.1 存件门店
        OrderComment depositComment = new OrderComment();
        depositComment.setOrderId(order.getId());
        depositComment.setOrderCode(order.getCode());
        depositComment.setMemberId(memberId);
        depositComment.setTargetType(Constants.ONE); // 1=存件门店
        depositComment.setTargetId(order.getDepositShopId());
        depositComment.setScore(dto.getDepositScore());
        depositComment.setContent(dto.getContent());
        depositComment.setDeleted(Constants.ZERO);
        depositComment.setCreateTime(now);
        orderCommentMapper.insert(depositComment);
        // 4.2 取件门店(异地寄存且有取件门店)
        if (isRemote && order.getTakeShopId() != null && dto.getTakeScore() != null) {
            OrderComment takeComment = new OrderComment();
            takeComment.setOrderId(order.getId());
            takeComment.setOrderCode(order.getCode());
            takeComment.setMemberId(memberId);
            takeComment.setTargetType(Constants.TWO); // 2=取件门店
            takeComment.setTargetId(order.getTakeShopId());
            takeComment.setScore(dto.getTakeScore());
            takeComment.setContent(dto.getContent());
            takeComment.setDeleted(Constants.ZERO);
            takeComment.setCreateTime(now);
            orderCommentMapper.insert(takeComment);
        }
        // 4.3 司机(异地寄存)
        if (isRemote && order.getAcceptDriver() != null && dto.getDriverScore() != null) {
            OrderComment driverComment = new OrderComment();
            driverComment.setOrderId(order.getId());
            driverComment.setOrderCode(order.getCode());
            driverComment.setMemberId(memberId);
            driverComment.setTargetType(Constants.THREE); // 3=司机
            driverComment.setTargetId(order.getAcceptDriver());
            driverComment.setScore(dto.getDriverScore());
            driverComment.setContent(dto.getContent());
            driverComment.setDeleted(Constants.ZERO);
            driverComment.setCreateTime(now);
            orderCommentMapper.insert(driverComment);
        }
        // 5. 更新门店/司机平均评分
        updateTargetScore(Constants.ONE, order.getDepositShopId());
        if (isRemote && order.getTakeShopId() != null) {
            updateTargetScore(Constants.TWO, order.getTakeShopId());
        }
        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());
    }
    /**
     * 更新评价对象(门店/司机)的平均评分
     */
    private void updateTargetScore(Integer targetType, Integer targetId) {
        List<OrderComment> comments = orderCommentMapper.selectList(new QueryWrapper<OrderComment>().lambda()
                .eq(OrderComment::getDeleted, Constants.ZERO)
                .eq(OrderComment::getTargetType, targetType)
                .eq(OrderComment::getTargetId, targetId));
        if (comments.isEmpty()) {
            return;
        }
        double avg = comments.stream()
                .mapToInt(OrderComment::getScore)
                .average()
                .orElse(0.0);
        BigDecimal score = BigDecimal.valueOf(avg).setScale(1, BigDecimal.ROUND_HALF_UP);
        Date now = new Date();
        if (Constants.equalsInteger(targetType, Constants.ONE) || Constants.equalsInteger(targetType, Constants.TWO)) {
            ShopInfo shopInfo = shopInfoMapper.selectById(targetId);
            if (shopInfo != null) {
                shopInfo.setScore(score);
                shopInfo.setUpdateTime(now);
                shopInfoMapper.updateById(shopInfo);
            }
        } else if (Constants.equalsInteger(targetType, Constants.THREE)) {
            DriverInfo driverInfo = driverInfoMapper.selectById(targetId);
            if (driverInfo != null) {
                driverInfo.setScore(score);
                driverInfo.setUpdateTime(now);
                driverInfoMapper.updateById(driverInfo);
            }
        }
    }
    /**
     * 唤起微信支付(其他订单)
     */
    private PayResponse wxPayForOtherOrder(OtherOrders otherOrders, String openid, Constants.OrdersAttach ordersAttach) {
        try {
            WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest();
            request.setBody(ordersAttach.getName());
            request.setAttach(ordersAttach.getKey());
            request.setOutTradeNo(otherOrders.getOutTradeNo());
            long totalFee = otherOrders.getPayAccount() != null ? otherOrders.getPayAccount() : 0L;
            request.setTotalFee((int) totalFee);
            request.setTimeStart(DateUtil.DateToString(new Date(), "yyyyMMddHHmmss"));
            request.setSpbillCreateIp(Constants.getIpAddr());
            request.setOpenid(openid);
            Object response = WxMiniConfig.wxPayService.createOrder(request);
            PayResponse payResponse = new PayResponse();
            payResponse.setResponse(response);
            payResponse.setOrderId(otherOrders.getId());
            return payResponse;
        } catch (WxPayException e) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "支付调起失败:" + e.getMessage());
        }
    }
    @Override
@@ -1646,6 +2364,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) {
@@ -1663,11 +2391,280 @@
            releaseVerifyCode(verifyCode);
            // 保存出库图片(obj_type=13 门店出库图片,最多3张)
            saveVerifyImages(order.getId(), images, Constants.FileType.STORE_OUT.getKey(), shopId);
            // 生成收益记录
            calculateAndSaveOrderFees(order.getId());
            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(), "当前订单状态不允许核销");
        }
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void confirmStoreOut(Integer orderId, Integer shopId, List<String> images, String remark) {
        // 1. 查询订单
        Orders order = ordersMapper.selectById(orderId);
        if (order == null || Constants.equalsInteger(order.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在");
        }
        // 2. 校验状态:待取件(5)
        if (!Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.arrived.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许出库");
        }
        // 3. 校验逾期状态:0=未逾期 或 2=已支付
        if (order.getOverdueStatus() != null && Constants.equalsInteger(order.getOverdueStatus(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "订单存在逾期未支付费用,请先完成逾期费用支付");
        }
        // 4. 校验确认到店时间不为空
        if (order.getConfirmArriveTime() == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "订单尚未确认到店,无法出库");
        }
        // 5. 校验门店与订单关系
        if (Constants.equalsInteger(order.getType(), Constants.ZERO)) {
            // 就地寄存:取件门店即存件门店
            if (!shopId.equals(order.getDepositShopId())) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单不属于当前门店");
            }
        } else {
            // 异地寄存:校验取件门店
            if (order.getTakeShopId() == null) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单无取件门店,无法出库");
            }
            if (!shopId.equals(order.getTakeShopId())) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单不属于当前门店");
            }
        }
        // 6. 查询门店名称
        String shopName = "";
        ShopInfo shopInfo = shopInfoMapper.selectById(shopId);
        if (shopInfo != null) {
            shopName = shopInfo.getName() != null ? shopInfo.getName() : "";
        }
        // 7. 更新订单状态为已完成
        Date now = new Date();
        order.setStatus(Constants.OrderStatus.finished.getStatus());
        order.setFinishTime(now);
        order.setUpdateTime(now);
        ordersMapper.updateById(order);
        // 8. 释放核销码
        if (StringUtils.isNotBlank(order.getMemberVerifyCode())) {
            releaseVerifyCode(order.getMemberVerifyCode());
        }
        // 9. 保存出库图片(obj_type=13 门店出库图片,最多3张)
        saveVerifyImages(order.getId(), images, Constants.FileType.STORE_OUT.getKey(), shopId);
        // 10. 如果存在退款金额,先保存退款记录再调用微信退款
        //    退款记录在退款调用前落库,避免退款成功但本地异常导致无记录
        if (order.getRefundAmount() != null && order.getRefundAmount() > 0
                && StringUtils.isNotBlank(order.getOutTradeNo())
                && order.getPayAmount() != null && order.getPayAmount() > 0) {
            OrdersRefund refundRecord = new OrdersRefund();
            refundRecord.setOrderId(orderId);
            refundRecord.setType(3); // 出库退款
            refundRecord.setCreateTime(now);
            refundRecord.setRefundRemark(remark);
            refundRecord.setDeleted(Constants.ZERO);
            ordersRefundMapper.insert(refundRecord);
            // 调用微信退款V3(放在最后,确保前置操作全部成功)
            Refund refundResult = wxPayV3Service.refund(
                    order.getOutTradeNo(), order.getPayAmount(), order.getRefundAmount(),
                    "订单退款", wxPayProperties.getV3RefundNotifyUrl());
            // 退款成功后回填退款单号,标记退款中
            refundRecord.setRefundCode(refundResult.getOutRefundNo());
            refundRecord.setStatus(Constants.ZERO); // 退款中
            ordersRefundMapper.updateById(refundRecord);
        }
        // 11. 生成收益记录
        calculateAndSaveOrderFees(orderId);
        generateRevenueRecords(orderId);
        // 12. 记录订单日志
        String logInfo = "门店【" + shopName + "】确认出库,订单完成";
        if (order.getRefundAmount() != null && order.getRefundAmount() > 0) {
            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
    public void calculateAndSaveOrderFees(Integer orderId) {
        Orders order = ordersMapper.selectById(orderId);
        if (order == null || Constants.equalsInteger(order.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在");
        }
        Long totalAmount = order.getTotalAmount() != null ? order.getTotalAmount() : 0L;
        // 费率(为空时默认0)
        BigDecimal depositRate = order.getDepositShopFeeRata() != null ? order.getDepositShopFeeRata() : BigDecimal.ZERO;
        BigDecimal takeRate = order.getTakeShopFeeRata() != null ? order.getTakeShopFeeRata() : BigDecimal.ZERO;
        BigDecimal driverRate = order.getDriverFeeRata() != null ? order.getDriverFeeRata() : BigDecimal.ZERO;
        Long exceptionFeeVal = order.getExceptionFee() != null ? order.getExceptionFee() : 0L;
        //存件门店收益
        Long depositShopFee = new BigDecimal(totalAmount)
                .multiply(depositRate)
                .setScale(0, RoundingMode.HALF_UP)
                .longValue();
        Long takeShopFee = 0L;
        Long driverFee = 0L;
        if (Constants.equalsInteger(order.getType(), Constants.TWO)) {
            // 异地寄存:存件门店 + 司机
            driverFee = new BigDecimal(totalAmount)
                    .multiply(driverRate)
                    .setScale(0, RoundingMode.HALF_UP)
                    .longValue()
                    + exceptionFeeVal;
            // 异地寄存且有取件门店:加上取件门店收益
            if (order.getTakeShopId() != null) {
                takeShopFee = new BigDecimal(totalAmount)
                        .multiply(takeRate)
                        .setScale(0, RoundingMode.HALF_UP)
                        .longValue();
            }
        }
        ordersMapper.update(new UpdateWrapper<Orders>().lambda()
                .eq(Orders::getId, orderId)
                .set(Orders::getDepositShopFee, depositShopFee)
                .set(Orders::getTakeShopFee, takeShopFee)
                .set(Orders::getDriverFee, driverFee)
                .set(Orders::getUpdateTime, new Date()));
    }
    /**
     * 生成门店/司机收益记录(未结算)
     * 订单完成时调用,读取订单上已计算好的费用字段
     */
    private void generateRevenueRecords(Integer orderId) {
        Orders order = ordersMapper.selectById(orderId);
        if (order == null) {
            return;
        }
        Date now = new Date();
        Long depositShopFee = order.getDepositShopFee() != null ? order.getDepositShopFee() : 0L;
        Long takeShopFee = order.getTakeShopFee() != null ? order.getTakeShopFee() : 0L;
        Long driverFee = order.getDriverFee() != null ? order.getDriverFee() : 0L;
        // 存件门店收益
        if (depositShopFee > 0 && order.getDepositShopId() != null) {
            ShopInfo depositShop = shopInfoMapper.selectById(order.getDepositShopId());
            if (depositShop != null && depositShop.getRegionMemberId() != null) {
                revenueMapper.insert(buildRevenue(depositShop.getRegionMemberId(), Constants.TWO,
                        depositShopFee, orderId, order.getCode()));
            }
        }
        // 取件门店收益(异地寄存且有取件门店)
        if (takeShopFee > 0 && order.getTakeShopId() != null) {
            ShopInfo takeShop = shopInfoMapper.selectById(order.getTakeShopId());
            if (takeShop != null && takeShop.getRegionMemberId() != null) {
                revenueMapper.insert(buildRevenue(takeShop.getRegionMemberId(), Constants.TWO,
                        takeShopFee, orderId, order.getCode()));
            }
        }
        // 司机收益(异地寄存)
        if (driverFee > 0 && order.getAcceptDriver() != null) {
            DriverInfo driver = driverInfoMapper.selectById(order.getAcceptDriver());
            if (driver != null && driver.getMemberId() != null) {
                revenueMapper.insert(buildRevenue(driver.getMemberId(), Constants.ONE,
                        driverFee, orderId, order.getCode()));
            }
        }
    }
    /**
     * 构建收益记录
     */
    private Revenue buildRevenue(Integer memberId, Integer memberType, Long amount, Integer orderId, String orderNo) {
        Revenue revenue = new Revenue();
        revenue.setMemberId(memberId);
        revenue.setMemberType(memberType); // 1=司机, 2=门店
        revenue.setType(Constants.ZERO); // 0=完成订单
        revenue.setOptType(Constants.ONE); // 1=收入
        revenue.setAmount(amount);
        revenue.setVaildStatus(Constants.ZERO); // 0=入账中(未结算)
        revenue.setObjId(orderId);
        revenue.setObjType(Constants.ZERO); // 0=订单业务
        revenue.setStatus(Constants.ZERO); // 0=成功
        revenue.setOrderNo(orderNo);
        revenue.setDeleted(Constants.ZERO);
        revenue.setCreateTime(new Date());
        return revenue;
    }
    @Override
@@ -1709,6 +2706,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);
        }
    }
    /**
@@ -1799,23 +2816,48 @@
                            + "天,逾期费用" + Constants.getFormatMoney(overdueInfo.getOverdueFee()) + "元",
                    null, shopId);
        } else {
            // 未逾期:完成订单
            order.setStatus(Constants.OrderStatus.finished.getStatus());
            // 未逾期:标记逾期状态为0,订单保持当前状态
            order.setConfirmArriveTime(now);
            order.setFinishTime(now);
            order.setOverdueStatus(Constants.ZERO);
            // 就地寄存:计算是否需要退款
            if (Constants.equalsInteger(order.getType(), Constants.ZERO) && !CollectionUtils.isEmpty(details)) {
                int actualDays = calcActualDepositDays(now, order.getDepositTime());
                order.setDepositDays(actualDays);
                int estimatedDays = order.getEstimatedDepositDays() != null ? order.getEstimatedDepositDays() : 1;
                int refundDays = estimatedDays - actualDays;
                if (refundDays > 0) {
                    // 退款金额 = 退款天数 × Σ(物品单价 × 数量)
                    long dailyBaseFee = 0L;
                    for (OrdersDetail d : details) {
                        dailyBaseFee += (d.getUnitPrice() != null ? d.getUnitPrice() : 0L)
                                * (d.getNum() != null ? d.getNum() : 0);
                    }
                    long refundAmount = (long) refundDays * dailyBaseFee;
                    order.setRefundAmount(refundAmount);
                }
            }
            order.setUpdateTime(now);
            ordersMapper.updateById(order);
            // 释放核销码
            if (StringUtils.isNotBlank(order.getMemberVerifyCode())) {
                releaseVerifyCode(order.getMemberVerifyCode());
            // 退款导致总金额变化,重算三方收益
            if (order.getRefundAmount() != null && order.getRefundAmount() > 0) {
                long newTotal = (order.getTotalAmount() != null ? order.getTotalAmount() : 0L) - order.getRefundAmount();
                order.setTotalAmount(newTotal);
                ordersMapper.update(new UpdateWrapper<Orders>().lambda()
                        .eq(Orders::getId, orderId)
                        .set(Orders::getTotalAmount, newTotal));
                calculateAndSaveOrderFees(orderId);
            }
            // 记录订单日志
            saveShopVerifyLog(order, "确认顾客到店",
                    "门店【" + shopName + "】确认顾客到店,订单完成",
                    null, shopId);
            String logInfo = "门店【" + shopName + "】确认顾客到店,未逾期";
            if (order.getRefundAmount() != null && order.getRefundAmount() > 0) {
                logInfo += ",需退款" + Constants.getFormatMoney(order.getRefundAmount()) + "元";
            }
            saveShopVerifyLog(order, "确认顾客到店", logInfo, null, shopId);
        }
    }
@@ -1972,6 +3014,32 @@
    }
    /**
     * 计算实际寄存天数(depositTime 到 now 的天数差,最少1天)
     */
    private int calcActualDepositDays(Date now, Date depositTime) {
        if (depositTime == null || now == null) {
            return 1;
        }
        Calendar depositCal = Calendar.getInstance();
        depositCal.setTime(depositTime);
        depositCal.set(Calendar.HOUR_OF_DAY, 0);
        depositCal.set(Calendar.MINUTE, 0);
        depositCal.set(Calendar.SECOND, 0);
        depositCal.set(Calendar.MILLISECOND, 0);
        Calendar nowCal = Calendar.getInstance();
        nowCal.setTime(now);
        nowCal.set(Calendar.HOUR_OF_DAY, 0);
        nowCal.set(Calendar.MINUTE, 0);
        nowCal.set(Calendar.SECOND, 0);
        nowCal.set(Calendar.MILLISECOND, 0);
        long diffMs = nowCal.getTimeInMillis() - depositCal.getTimeInMillis();
        int days = (int) (diffMs / (1000 * 60 * 60 * 24));
        return Math.max(days, 1);
    }
    /**
     * 就地寄存逾期天数计算
     * 过了预计取件时间当天的12点后才算一天
     */
@@ -2052,4 +3120,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;
    }
}