DeliveryOrderServiceImpl.java 11 KB
package com.diligrp.rider.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.diligrp.rider.entity.MerchantStore;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.diligrp.rider.common.exception.BizException;
import com.diligrp.rider.dto.DeliveryFeeCalcDTO;
import com.diligrp.rider.dto.DeliveryOrderCreateDTO;
import com.diligrp.rider.entity.OpenApp;
import com.diligrp.rider.entity.Orders;
import com.diligrp.rider.mapper.OpenAppMapper;
import com.diligrp.rider.mapper.OrdersMapper;
import com.diligrp.rider.service.DeliveryFeeService;
import com.diligrp.rider.service.DeliveryOrderService;
import com.diligrp.rider.service.MerchantService;
import com.diligrp.rider.service.WebhookService;
import com.diligrp.rider.vo.DeliveryFeeResultVO;
import com.diligrp.rider.vo.DeliveryOrderCreateVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
public class DeliveryOrderServiceImpl implements DeliveryOrderService {

    private final OrdersMapper ordersMapper;
    private final OpenAppMapper openAppMapper;
    private final MerchantService merchantService;
    private final DeliveryFeeService deliveryFeeService;
    private final WebhookService webhookService;
    private final ObjectMapper objectMapper;

    @Override
    @Transactional
    public DeliveryOrderCreateVO create(String appKey, DeliveryOrderCreateDTO dto) {
        // 1. 校验应用,从 AppKey 获取租户 cityId(不信任调用方传入的 cityId)
        OpenApp app = getApp(appKey);
        if (app.getCityId() == null || app.getCityId() < 1) {
            throw new BizException("该应用未绑定城市/租户,请联系平台管理员");
        }
        // 强制使用 AppKey 绑定的 cityId,覆盖调用方传入的值
        dto.setCityId(app.getCityId());

        // 2. 检查外部订单号唯一性
        Long exists = ordersMapper.selectCount(new LambdaQueryWrapper<Orders>()
                .eq(Orders::getAppKey, appKey)
                .eq(Orders::getOutOrderNo, dto.getOutOrderNo()));
        if (exists > 0) throw new BizException("外部订单号已存在:" + dto.getOutOrderNo());

        // 3. 若传了 outStoreId,用 merchant_store 里的门店信息补全发货方
        if (dto.getOutStoreId() != null && !dto.getOutStoreId().isBlank()) {
            MerchantStore ms = merchantService.getByOutStoreId(appKey, dto.getOutStoreId());
            if (ms != null) {
                if (dto.getStoreName() == null || dto.getStoreName().isBlank())
                    dto.setStoreName(ms.getName());
                if (dto.getStoreAddr() == null || dto.getStoreAddr().isBlank())
                    dto.setStoreAddr(ms.getAddress());
                if (dto.getStoreLng() == null || dto.getStoreLng().isBlank())
                    dto.setStoreLng(ms.getLng());
                if (dto.getStoreLat() == null || dto.getStoreLat().isBlank())
                    dto.setStoreLat(ms.getLat());
            }
        }
        // 校验发货方坐标
        if (dto.getStoreLng() == null || dto.getStoreLng().isBlank()
                || dto.getStoreLat() == null || dto.getStoreLat().isBlank()) {
            throw new BizException("发货方经纬度不能为空(请传 storeLng/storeLat 或有效的 outStoreId)");
        }

        // 4. 计算配送费
        DeliveryFeeCalcDTO feeDTO = new DeliveryFeeCalcDTO();
        feeDTO.setCityId(dto.getCityId());
        feeDTO.setOrderType(6); // 外卖配送
        feeDTO.setStartLng(dto.getStoreLng());
        feeDTO.setStartLat(dto.getStoreLat());
        feeDTO.setEndLng(dto.getRecipLng());
        feeDTO.setEndLat(dto.getRecipLat());
        feeDTO.setWeight(dto.getWeight());
        feeDTO.setPieces(calcPieces(dto.getItems()));
        feeDTO.setServiceTime(dto.getServiceTime());
        DeliveryFeeResultVO fee = deliveryFeeService.calcFee(feeDTO);

        // 4. 生成订单号
        String orderNo = generateOrderNo();
        // 5. 生成完成码(6位数字)
        String code = String.valueOf((int) (Math.random() * 900000 + 100000));

        // 6. 构建 extra 信息
        Map<String, Object> extra = new HashMap<>();
        extra.put("distance", fee.getDistance());
        extra.put("weight", dto.getWeight());
        extra.put("pieces", feeDTO.getPieces());
        extra.put("remark", dto.getRemark());
        extra.put("computed", Map.of(
                "money_basic", fee.getMoneyBasic(),
                "money_distance", fee.getMoneyDistance(),
                "money_piece", fee.getMoneyPiece(),
                "money_time", fee.getMoneyTime()
        ));
        String extraJson;
        try {
            extraJson = objectMapper.writeValueAsString(extra);
        } catch (Exception e) {
            extraJson = "{}";
        }

        // 7. 回调地址优先用订单级,其次用应用级
        String callbackUrl = dto.getCallbackUrl();
        if (callbackUrl == null || callbackUrl.isBlank()) {
            callbackUrl = app.getWebhookUrl();
        }

        // 8. 创建订单
        Orders order = new Orders();
        order.setOrderNo(orderNo);
        order.setOutOrderNo(dto.getOutOrderNo());
        order.setAppKey(appKey);
        order.setCityId(dto.getCityId());
        order.setType(6);
        order.setStatus(2); // 直接到待接单(外部系统已完成支付)
        order.setMoneyDelivery(fee.getTotalFee());
        order.setMoney(fee.getTotalFee());
        order.setMoneyTotal(fee.getTotalFee());
        order.setFName(dto.getStoreName());
        order.setFAddr(dto.getStoreAddr() != null ? dto.getStoreAddr() : "");
        order.setFLng(dto.getStoreLng());
        order.setFLat(dto.getStoreLat());
        order.setTName(dto.getRecipAddr());
        order.setTAddr(dto.getRecipAddr());
        order.setTLng(dto.getRecipLng());
        order.setTLat(dto.getRecipLat());
        order.setRecipName(dto.getRecipName());
        order.setRecipPhone(dto.getRecipPhone());
        order.setCode(code);
        order.setExtra(extraJson);
        order.setCallbackUrl(callbackUrl);
        order.setItemsJson(null);
        // 货物快照
        if (dto.getItems() != null && !dto.getItems().isEmpty()) {
            try {
                order.setItemsJson(objectMapper.writeValueAsString(dto.getItems()));
            } catch (Exception ignored) {}
        }
        order.setItemRemark(dto.getItemRemark());
        order.setExtStoreId(null); // 改为通过 outStoreId 关联,不存具体ID
        order.setRiderId(0L);
        order.setOldRiderId(0L);
        order.setIsTrans(0);
        order.setIsIncome(0);
        order.setIsDel(0);
        order.setAddTime(System.currentTimeMillis() / 1000);
        order.setPayTime(System.currentTimeMillis() / 1000);
        ordersMapper.insert(order);

        // 9. 回调接入方:订单已创建
        notifyCallback(order, "order.created");

        return toCreateVO(order, fee);
    }

    @Override
    public DeliveryOrderCreateVO queryByOutOrderNo(String appKey, String outOrderNo) {
        getApp(appKey);
        Orders order = getOrderByOutNo(appKey, outOrderNo);
        DeliveryFeeResultVO feeVO = new DeliveryFeeResultVO();
        feeVO.setMoneyBasic(order.getMoneyDelivery());
        feeVO.setMoneyDistance(BigDecimal.ZERO);
        feeVO.setMoneyTime(BigDecimal.ZERO);
        feeVO.setTotalFee(order.getMoneyDelivery());
        return toCreateVO(order, feeVO);
    }

    @Override
    @Transactional
    public void cancel(String appKey, String outOrderNo) {
        getApp(appKey);
        Orders order = getOrderByOutNo(appKey, outOrderNo);
        if (order.getStatus() != 2) {
            throw new BizException("订单已接单或已完成,无法取消");
        }
        ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
                .eq(Orders::getId, order.getId())
                .set(Orders::getStatus, 10));
        order.setStatus(10);
        notifyCallback(order, "order.cancelled");
    }

    // ---- 私有方法 ----

    private OpenApp getApp(String appKey) {
        OpenApp app = openAppMapper.selectOne(new LambdaQueryWrapper<OpenApp>()
                .eq(OpenApp::getAppKey, appKey).last("LIMIT 1"));
        if (app == null || app.getStatus() != 1) throw new BizException("应用不存在或已禁用");
        return app;
    }

    private Orders getOrderByOutNo(String appKey, String outOrderNo) {
        Orders order = ordersMapper.selectOne(new LambdaQueryWrapper<Orders>()
                .eq(Orders::getAppKey, appKey)
                .eq(Orders::getOutOrderNo, outOrderNo)
                .last("LIMIT 1"));
        if (order == null) throw new BizException("订单不存在");
        return order;
    }

    private void notifyCallback(Orders order, String event) {
        try {
            Map<String, Object> payload = new HashMap<>();
            payload.put("event", event);
            payload.put("outOrderNo", order.getOutOrderNo());
            payload.put("deliveryOrderId", order.getId());
            payload.put("orderNo", order.getOrderNo());
            payload.put("status", order.getStatus());
            payload.put("timestamp", System.currentTimeMillis() / 1000);
            String json = objectMapper.writeValueAsString(payload);
            webhookService.send(event, order.getId(), json);
        } catch (Exception e) {
            log.warn("通知回调失败 orderId={}", order.getId(), e);
        }
    }

    private String generateOrderNo() {
        String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        return "DL" + date + UUID.randomUUID().toString().replace("-", "").substring(0, 10).toUpperCase();
    }

    private Integer calcPieces(List<DeliveryOrderCreateDTO.DeliveryItemDTO> items) {
        if (items == null || items.isEmpty()) {
            return 0;
        }
        int pieces = 0;
        for (DeliveryOrderCreateDTO.DeliveryItemDTO item : items) {
            pieces += item.getQuantity() != null && item.getQuantity() > 0 ? item.getQuantity() : 1;
        }
        return pieces;
    }

    private DeliveryOrderCreateVO toCreateVO(Orders order, DeliveryFeeResultVO fee) {
        DeliveryOrderCreateVO vo = new DeliveryOrderCreateVO();
        vo.setDeliveryOrderId(order.getId());
        vo.setOrderNo(order.getOrderNo());
        vo.setOutOrderNo(order.getOutOrderNo());
        vo.setMoneyBasic(fee.getMoneyBasic());
        vo.setMoneyDistance(fee.getMoneyDistance());
        vo.setMoneyTime(fee.getMoneyTime());
        vo.setTotalFee(fee.getTotalFee());
        vo.setDistance(fee.getDistance());
        vo.setEstimatedMinutes(fee.getEstimatedMinutes());
        vo.setStatus(order.getStatus());
        return vo;
    }
}