KKSU
2025-02-11 e8f8d89a4248cd4d0a7138cc2e5a36ea9b136699
feat(mall): 添加订单一键发货和取消发货功能

- 在订单列表页面添加一键发货和取消发货按钮
- 实现订单一键发货和取消发货的后端接口
- 添加发货人信息配置页面和相关功能
- 集成PDF.js用于渲染电子面单
- 优化订单列表页面的显示逻辑
9 files modified
5 files added
793 ■■■■ changed files
src/main/java/cc/mrbird/febs/common/enumerates/DataDictionaryEnum.java 6 ●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/controller/AdminMallOrderController.java 24 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/controller/ViewSystemController.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/service/IAdminMallOrderService.java 4 ●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/service/impl/AdminMallOrderService.java 108 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/service/impl/CommonService.java 74 ●●●● patch | view | raw | blame | history
src/main/java/com/best/javaSdk/ClientParamEnum.java 34 ●●●●● patch | view | raw | blame | history
src/main/java/com/best/javaSdk/ClientParamService.java 39 ●●●●● patch | view | raw | blame | history
src/main/java/com/best/javaSdk/ClientService.java 17 ●●●●● patch | view | raw | blame | history
src/main/java/com/best/javaSdk/ClientServiceImpl.java 63 ●●●●● patch | view | raw | blame | history
src/main/resources/templates/febs/views/modules/order/orderList.html 201 ●●●●● patch | view | raw | blame | history
src/main/resources/templates/febs/views/modules/system/sender.html 97 ●●●●● patch | view | raw | blame | history
src/main/resources/templates/index.html 2 ●●● patch | view | raw | blame | history
src/test/java/cc/mrbird/febs/AgentTest.java 119 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/common/enumerates/DataDictionaryEnum.java
@@ -1,11 +1,15 @@
package cc.mrbird.febs.common.enumerates;
import lombok.Data;
import lombok.Getter;
@Getter
public enum DataDictionaryEnum {
    // 发货人
    SENDER_NAME("SENDER_SET", "NAME"),
    SENDER_MOBILE("SENDER_SET", "MOBILE"),
    SENDER_ADDRESS("SENDER_SET", "ADDRESS"),
    // 发票的通知回调路径
    FP_CALLBACK_URL("FP_CALLBACK_URL", "FP_CALLBACK_URL"),
    //微信订阅模板ID,
src/main/java/cc/mrbird/febs/mall/controller/AdminMallOrderController.java
@@ -135,6 +135,30 @@
    }
    /**
     * 订单列表-取消发货
     *
     * @param id
     * @return
     */
    @GetMapping("cancelDeliver/{id}")
    @ControllerEndpoint(operation = "订单列表-取消发货", exceptionMessage = "操作失败")
    public FebsResponse cancelDeliver(@NotNull(message = "{required}") @PathVariable Long id) {
        return adminMallOrderService.cancelDeliver(id);
    }
    /**
     * 订单列表-一键发货
     *
     * @param id
     * @return
     */
    @GetMapping("deliverPdfGoods/{id}")
    @ControllerEndpoint(operation = "订单列表-一键发货", exceptionMessage = "操作失败")
    public FebsResponse deliverPdfGoods(@NotNull(message = "{required}") @PathVariable Long id) {
        return adminMallOrderService.deliverPdfGoods(id);
    }
    /**
     * 订单退款-列表
     *
     * @param mallOrderRefundDto
src/main/java/cc/mrbird/febs/mall/controller/ViewSystemController.java
@@ -124,4 +124,9 @@
    public String kefu() {
        return FebsUtil.view("modules/system/kefu");
    }
    @GetMapping("sender")
    public String sender() {
        return FebsUtil.view("modules/system/sender");
    }
}
src/main/java/cc/mrbird/febs/mall/service/IAdminMallOrderService.java
@@ -76,4 +76,8 @@
    IPage<AdminGoodsStatisticsVo> goodsStatistics(MallOrderItem mallOrderItem, QueryRequest request);
    FebsResponse deliverGoodsUpdate(DeliverGoodsDto deliverGoodsDto);
    FebsResponse deliverPdfGoods(Long id);
    FebsResponse cancelDeliver(Long id);
}
src/main/java/cc/mrbird/febs/mall/service/impl/AdminMallOrderService.java
@@ -2,10 +2,8 @@
import cc.mrbird.febs.common.entity.FebsResponse;
import cc.mrbird.febs.common.entity.QueryRequest;
import cc.mrbird.febs.common.enumerates.FlowTypeEnum;
import cc.mrbird.febs.common.enumerates.MoneyFlowTypeEnum;
import cc.mrbird.febs.common.enumerates.OrderDeliveryStateEnum;
import cc.mrbird.febs.common.enumerates.OrderStatusEnum;
import cc.mrbird.febs.common.enumerates.*;
import cc.mrbird.febs.common.utils.ValidateEntityUtils;
import cc.mrbird.febs.mall.dto.*;
import cc.mrbird.febs.mall.entity.*;
import cc.mrbird.febs.mall.mapper.*;
@@ -19,12 +17,19 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.best.javaSdk.ClientParamEnum;
import com.best.javaSdk.ClientParamService;
import com.best.javaSdk.kdCancelOrderNotify.request.KdCancelOrderNotifyReq;
import com.best.javaSdk.kdCancelOrderNotify.response.KdCancelOrderNotifyRsp;
import com.best.javaSdk.kdCreateWaybillOrderPdfNotify.request.*;
import com.best.javaSdk.kdCreateWaybillOrderPdfNotify.response.KdCreateWaybillOrderPdfNotifyRsp;
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.util.ArrayList;
import java.util.List;
@Slf4j
@@ -490,6 +495,101 @@
    }
    @Override
    public FebsResponse deliverPdfGoods(Long id) {
        MallOrderInfo mallOrderInfo = ValidateEntityUtils.ensureColumnReturnEntity(id, MallOrderInfo::getId, mallOrderInfoMapper::selectOne, "订单不存在");
        ValidateEntityUtils.ensureEqual(mallOrderInfo.getStatus(), OrderStatusEnum.WAIT_SHIPPING.getValue(), "订单不是待发货状态");
        KdCreateWaybillOrderPdfNotifyReq kdCreateWaybillOrderPdfNotifyReq = new KdCreateWaybillOrderPdfNotifyReq();
        MallMember mallMember = ValidateEntityUtils.ensureColumnReturnEntity(mallOrderInfo.getMemberId(), MallMember::getId, mallMemberMapper::selectOne, "会员不存在");
        kdCreateWaybillOrderPdfNotifyReq.setTxLogisticId(mallOrderInfo.getOrderNo());
        kdCreateWaybillOrderPdfNotifyReq.setServiceType("1");//服务类型(0-线下下单,1-线上下单)
        kdCreateWaybillOrderPdfNotifyReq.setSpecial("1");
        //发货人信息
        Sender sender = new Sender();
        sender.setName(dataDictionaryCustomMapper.selectDicDataByTypeAndCode(
                DataDictionaryEnum.SENDER_NAME.getType(),
                DataDictionaryEnum.SENDER_NAME.getCode()
        ).getValue());
        sender.setMobile(dataDictionaryCustomMapper.selectDicDataByTypeAndCode(
                DataDictionaryEnum.SENDER_MOBILE.getType(),
                DataDictionaryEnum.SENDER_MOBILE.getCode()
        ).getValue());
        sender.setAddress(dataDictionaryCustomMapper.selectDicDataByTypeAndCode(
                DataDictionaryEnum.SENDER_ADDRESS.getType(),
                DataDictionaryEnum.SENDER_ADDRESS.getCode()
        ).getValue());
        kdCreateWaybillOrderPdfNotifyReq.setSender(sender);
        //收件人
        Receiver receiver = new Receiver();
        receiver.setName(mallMember.getRealName());
        receiver.setMobile(mallMember.getPhone());
        receiver.setAddress(mallOrderInfo.getAddress());
        kdCreateWaybillOrderPdfNotifyReq.setReceiver(receiver);
        //包裹
        Items items = new Items();
        ArrayList<Item> itemList = new ArrayList<>();
        List<MallOrderItem> mallOrderItemList = ValidateEntityUtils
                .ensureColumnReturnEntityList(id, MallOrderItem::getOrderId, mallOrderItemMapper::selectList, "订单不存在");
        StringBuffer itemName = new StringBuffer();
        itemName.append("商品:");
        mallOrderItemList.forEach(mallOrderItem -> {
            itemName.append(mallOrderItem.getGoodsName()+"-"+mallOrderItem.getSkuName());
        });
        Item item = new Item();
        item.setItemName(itemName.toString());
        itemList.add(item);
        items.setItem(itemList);
        kdCreateWaybillOrderPdfNotifyReq.setItems(items);
        kdCreateWaybillOrderPdfNotifyReq.setPiece(1);
        KdCreateWaybillOrderPdfNotifyRsp pdfOrder = ClientParamService.getInstance(ClientParamEnum.TEST.name()).createPdfOrder(kdCreateWaybillOrderPdfNotifyReq);
        ValidateEntityUtils.ensureEqual(pdfOrder.getResult(), true, "一键发货失败,创建PDF电子面单异常");
        //更新发货状态
        mallOrderInfoMapper.updateOrderStateAndDeliveryState(
                mallOrderInfo.getId(),
                OrderStatusEnum.WAIT_FINISH.getValue(),
                OrderDeliveryStateEnum.DELIVERY_ING.getValue());
        MallExpressInfo mallExpressInfo = new MallExpressInfo();
        mallExpressInfo.setMemberId(mallOrderInfo.getMemberId());
        mallExpressInfo.setOrderId(mallOrderInfo.getId());
        mallExpressInfo.setExpressNo(pdfOrder.getMailNo());
        mallExpressInfo.setExpressCom("Best Logistic");
        mallExpressInfo.setExpressCode("Best Logistic");
        mallExpressInfoMapper.insert(mallExpressInfo);
        return new FebsResponse().success().data(pdfOrder);
    }
    @Override
    public FebsResponse cancelDeliver(Long id) {
        MallOrderInfo mallOrderInfo = ValidateEntityUtils.ensureColumnReturnEntity(id, MallOrderInfo::getId, mallOrderInfoMapper::selectOne, "订单不存在");
        ValidateEntityUtils.ensureEqual(mallOrderInfo.getStatus(), OrderStatusEnum.WAIT_FINISH.getValue(), "订单不是待收货状态");
        KdCancelOrderNotifyReq kdCancelOrderNotifyReq = new KdCancelOrderNotifyReq();
        kdCancelOrderNotifyReq.setTxLogisticId(mallOrderInfo.getOrderNo());
        kdCancelOrderNotifyReq.setReason("Don't want to buy");
        KdCancelOrderNotifyRsp kdCancelOrderNotifyRsp = ClientParamService.getInstance(ClientParamEnum.TEST.name()).cancelOrder(kdCancelOrderNotifyReq);
        ValidateEntityUtils.ensureEqual(kdCancelOrderNotifyRsp.getResult(), true, "取消发货失败");
        //更新发货状态
        mallOrderInfoMapper.updateOrderStateAndDeliveryState(
                mallOrderInfo.getId(),
                OrderStatusEnum.WAIT_SHIPPING.getValue(),
                OrderDeliveryStateEnum.DELIVERY_WAIT.getValue());
        List<MallExpressInfo> mallExpressInfoList = ValidateEntityUtils.ensureColumnReturnEntityList(mallOrderInfo.getId(), MallExpressInfo::getOrderId, mallExpressInfoMapper::selectList, "未查询到物流信息");
        mallExpressInfoList.forEach(mallExpressInfo -> mallExpressInfoMapper.deleteById(mallExpressInfo.getId()));
        return new FebsResponse().success().message("取消发货成功,请重新发货");
    }
    @Override
    public void deliverGoodsByOrderNo(DeliverGoodsDto deliverGoodsDto) {
        MallOrderInfo mallOrderInfo = mallOrderInfoMapper.selectByOrderNo(deliverGoodsDto.getOrderNo());
        if (mallOrderInfo == null) {
src/main/java/cc/mrbird/febs/mall/service/impl/CommonService.java
@@ -11,15 +11,13 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.best.javaSdk.Client;
import com.best.javaSdk.kdTraceQuery.request.KdTraceQueryReq;
import com.best.javaSdk.kdTraceQuery.request.MailNos;
import com.best.javaSdk.ClientParamEnum;
import com.best.javaSdk.ClientParamService;
import com.best.javaSdk.kdTraceQuery.response.KdTraceQueryRsp;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
@@ -107,39 +105,43 @@
        dataDictionaryCustomMapper.insert(dic);
    }
//    @Override
//    public KdTraceQueryRsp checkTraceInfo(ApiCheckTraceInfoDto checkTraceInfoDto) {
//        /**
//         * 正式环境
//         * MY_LEADING 生产环境参数
//         *               partnerID   : MY_LEADING
//         *               partnerKey  :  ER5DFRT320D4ed6FAFs3G410Fs977
//         *               Endpoint    :http://sgp-seaedi.800best.com/Malaysia/kdapi/api/process
//         */
//        String url = "http://sgp-seaedi.800best.com/Malaysia/kdapi/api/process";
//        String partnerID = "MY_LEADING";
//        String partnerKey = "ER5DFRT320D4ed6FAFs3G410Fs977";
//        String format = "JSON";
//        /**
//         * 测试环境
//         *  测试物流编号 66660451238000
//         */
////        String url = "http://sea-edi-hxtest.800best.com/Malaysia/kdapi/api/process";
////        String partnerID = "M_TEST";
////        String partnerKey = "TEST12345";
////        String format = "JSON";
//
//        Client client = new Client(url, partnerID, partnerKey, format);
//
//        KdTraceQueryReq tdTraceQueryReq = new KdTraceQueryReq();
//        MailNos mailNos = new MailNos();
//        List<String> mailNo = new ArrayList<>();
//        mailNo.add(checkTraceInfoDto.getTraceNo());
//        mailNos.setMailNo(mailNo);
//        tdTraceQueryReq.setMailNos(mailNos);
//        tdTraceQueryReq.setLangType("zh-CN");
//
//        KdTraceQueryRsp kdTraceQueryRsp = client.executed(tdTraceQueryReq);
//        return kdTraceQueryRsp;
//    }
    @Override
    public KdTraceQueryRsp checkTraceInfo(ApiCheckTraceInfoDto checkTraceInfoDto) {
        /**
         * 正式环境
         * MY_LEADING 生产环境参数
         *               partnerID   : MY_LEADING
         *               partnerKey  :  ER5DFRT320D4ed6FAFs3G410Fs977
         *               Endpoint    :http://sgp-seaedi.800best.com/Malaysia/kdapi/api/proces
         */
        String url = "http://sgp-seaedi.800best.com/Malaysia/kdapi/api/process";
        String partnerID = "MY_LEADING";
        String partnerKey = "ER5DFRT320D4ed6FAFs3G410Fs977";
        String format = "JSON";
        /**
         * 测试环境
         *  测试物流编号 66660451238000
         */
//        String url = "http://sea-edi-hxtest.800best.com/Malaysia/kdapi/api/process";
//        String partnerID = "M_TEST";
//        String partnerKey = "TEST12345";
//        String format = "JSON";
        Client client = new Client(url, partnerID, partnerKey, format);
        KdTraceQueryReq tdTraceQueryReq = new KdTraceQueryReq();
        MailNos mailNos = new MailNos();
        List<String> mailNo = new ArrayList<>();
        mailNo.add(checkTraceInfoDto.getTraceNo());
        mailNos.setMailNo(mailNo);
        tdTraceQueryReq.setMailNos(mailNos);
        tdTraceQueryReq.setLangType("zh-CN");
        KdTraceQueryRsp kdTraceQueryRsp = client.executed(tdTraceQueryReq);
        return kdTraceQueryRsp;
        return ClientParamService.getInstance(ClientParamEnum.PRD.name()).checkTraceInfo(checkTraceInfoDto);
    }
}
src/main/java/com/best/javaSdk/ClientParamEnum.java
New file
@@ -0,0 +1,34 @@
package com.best.javaSdk;
import lombok.Getter;
@Getter
public enum ClientParamEnum {
    TEST(
            "http://sea-edi-hxtest.800best.com/Malaysia/kdapi/api/process",
            "M_TEST",
            "TEST12345",
            "JSON"
    ),
    PRD(
            "http://sgp-seaedi.800best.com/Malaysia/kdapi/api/process",
            "MY_LEADING",
            "ER5DFRT320D4ed6FAFs3G410Fs977",
            "JSON"
    );
    private String url;
    private String partnerID;
    private String partnerKey;
    private String messageFormat;
    ClientParamEnum(String url, String partnerID, String partnerKey, String messageFormat) {
        this.url = url;
        this.partnerID = partnerID;
        this.partnerKey = partnerKey;
        this.messageFormat = messageFormat;
    }
}
src/main/java/com/best/javaSdk/ClientParamService.java
New file
@@ -0,0 +1,39 @@
package com.best.javaSdk;
import cc.mrbird.febs.common.exception.FebsException;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class ClientParamService {
    private final static Map<String, ClientServiceImpl> client_map = new HashMap<>();
    static {
        for (ClientParamEnum clientParamEnum : ClientParamEnum.values()) {
            client_map.put(
                    clientParamEnum.name(),
                    new ClientServiceImpl(
                            clientParamEnum.getUrl(),
                            clientParamEnum.getPartnerID(),
                            clientParamEnum.getPartnerKey(),
                            clientParamEnum.getMessageFormat()));
        }
    }
    private ClientParamService() {
    }
    public final static ClientParamService INSTANCE = new ClientParamService();
    public static ClientService getInstance(String type) {
        ClientServiceImpl clientService = client_map.get(type);
        if (clientService == null) {
            throw new FebsException("参数错误");
        }
        return clientService;
    }
}
src/main/java/com/best/javaSdk/ClientService.java
New file
@@ -0,0 +1,17 @@
package com.best.javaSdk;
import cc.mrbird.febs.mall.dto.ApiCheckTraceInfoDto;
import com.best.javaSdk.kdCancelOrderNotify.request.KdCancelOrderNotifyReq;
import com.best.javaSdk.kdCancelOrderNotify.response.KdCancelOrderNotifyRsp;
import com.best.javaSdk.kdCreateWaybillOrderPdfNotify.request.KdCreateWaybillOrderPdfNotifyReq;
import com.best.javaSdk.kdCreateWaybillOrderPdfNotify.response.KdCreateWaybillOrderPdfNotifyRsp;
import com.best.javaSdk.kdTraceQuery.response.KdTraceQueryRsp;
public interface ClientService {
    KdTraceQueryRsp checkTraceInfo(ApiCheckTraceInfoDto checkTraceInfoDto);
    KdCreateWaybillOrderPdfNotifyRsp createPdfOrder(KdCreateWaybillOrderPdfNotifyReq kdCreateWaybillOrderPdfNotifyReq);
    KdCancelOrderNotifyRsp cancelOrder(KdCancelOrderNotifyReq kdCancelOrderNotifyReq);
}
src/main/java/com/best/javaSdk/ClientServiceImpl.java
New file
@@ -0,0 +1,63 @@
package com.best.javaSdk;
import cc.mrbird.febs.mall.dto.ApiCheckTraceInfoDto;
import cn.hutool.json.JSONUtil;
import com.best.javaSdk.kdCancelOrderNotify.request.KdCancelOrderNotifyReq;
import com.best.javaSdk.kdCancelOrderNotify.response.KdCancelOrderNotifyRsp;
import com.best.javaSdk.kdCreateWaybillOrderPdfNotify.request.KdCreateWaybillOrderPdfNotifyReq;
import com.best.javaSdk.kdCreateWaybillOrderPdfNotify.response.KdCreateWaybillOrderPdfNotifyRsp;
import com.best.javaSdk.kdTraceQuery.request.KdTraceQueryReq;
import com.best.javaSdk.kdTraceQuery.request.MailNos;
import com.best.javaSdk.kdTraceQuery.response.KdTraceQueryRsp;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class ClientServiceImpl implements ClientService{
    private Client client;
    private String url;
    private String partnerID;
    private String partnerKey;
    private String messageFormat;
    public ClientServiceImpl(String url, String partnerID, String partnerKey, String messageFormat) {
        this.url = url;
        this.partnerID = partnerID;
        this.partnerKey = partnerKey;
        this.messageFormat = messageFormat;
        client = new Client(url, partnerID, partnerKey, messageFormat);
    }
    @Override
    public KdTraceQueryRsp checkTraceInfo(ApiCheckTraceInfoDto checkTraceInfoDto) {
        KdTraceQueryReq tdTraceQueryReq = new KdTraceQueryReq();
        MailNos mailNos = new MailNos();
        List<String> mailNo = new ArrayList<>();
        mailNo.add(checkTraceInfoDto.getTraceNo());
        mailNos.setMailNo(mailNo);
        tdTraceQueryReq.setMailNos(mailNos);
        tdTraceQueryReq.setLangType("zh-CN");
        KdTraceQueryRsp kdTraceQueryRsp = client.executed(tdTraceQueryReq);
        return kdTraceQueryRsp;
    }
    @Override
    public KdCreateWaybillOrderPdfNotifyRsp createPdfOrder(KdCreateWaybillOrderPdfNotifyReq kdCreateWaybillOrderPdfNotifyReq) {
        KdCreateWaybillOrderPdfNotifyRsp executed = client.executed(kdCreateWaybillOrderPdfNotifyReq);
        log.info("executed:{}", JSONUtil.parseObj(executed));
        return executed;
    }
    @Override
    public KdCancelOrderNotifyRsp cancelOrder(KdCancelOrderNotifyReq kdCancelOrderNotifyReq) {
        KdCancelOrderNotifyRsp executed = client.executed(kdCancelOrderNotifyReq);
        log.info("executed:{}", JSONUtil.parseObj(executed));
        return executed;
    }
}
src/main/resources/templates/febs/views/modules/order/orderList.html
@@ -356,6 +356,16 @@
                    cancelOrder(data.id);
                });
            }
            if (layEvent === 'deliverPdfGoods') {
                febs.modal.confirm('一键发货', '确认一键发货?', function () {
                    deliverPdfGoods(data.id);
                });
            }
            if (layEvent === 'cancelDeliver') {
                febs.modal.confirm('取消发货', '确认取消发货?', function () {
                    cancelDeliver(data.id);
                });
            }
            if (layEvent === 'seePayImage') {
                var t = $view.find('#seePayImage'+data.id+'');
                //页面层
@@ -373,12 +383,195 @@
            }
        });
        function cancelDeliver(id) {
            febs.get(ctx + 'admin/order/cancelDeliver/' + id, null, function (data) {
                febs.alert.success(data.message);
                $query.click();
            });
        }
        function cancelOrder(id) {
            febs.get(ctx + 'admin/order/cancelOrder/' + id, null, function () {
                febs.alert.success('操作成功');
                $query.click();
            });
        }
        function deliverPdfGoods(id) {
            febs.get(ctx + 'admin/order/deliverPdfGoods/' + id, null, function (e) {
                if (e.code == 200) {
                    // 创建弹层容器
                    const content = `
                                        <div style="position:relative;min-height:500px">
                                          <div id="pdfContainer" style="margin:20px auto;max-width:800px"></div>
                                          <div class="layui-form" style="text-align:center;margin-top:20px">
                                            <button class="layui-btn layui-btn-normal" onclick="printPdf()">打印</button>
                                          </div>
                                        </div>
                                      `;
                    // 显示弹层
                    const index = layer.open({
                        type: 1,
                        title: "电子面单",
                        content: content,
                        area: ['90%', '90%'],
                        success: function() {
                            // 渲染PDF
                            renderPdf(e.data.pdfStream);
                        }
                    });
                    // 存储当前PDF数据
                    window.currentPDF = e.data.pdfStream;
                } else {
                    febs.alert.warn(e.message);
                }
            });
            $query.click();
        }
        // PDF渲染函数
        function renderPdf(base64Data) {
            // 清理容器
            const container = document.getElementById('pdfContainer');
            container.innerHTML = '';
            // Base64转Blob
            const byteCharacters = atob(base64Data);
            const byteNumbers = new Array(byteCharacters.length);
            for (let i = 0; i < byteCharacters.length; i++) {
                byteNumbers[i] = byteCharacters.charCodeAt(i);
            }
            const byteArray = new Uint8Array(byteNumbers);
            const blob = new Blob([byteArray], {type: 'application/pdf'});
            // 生成临时URL
            const url = URL.createObjectURL(blob);
            // 使用PDF.js渲染
            pdfjsLib.getDocument(url).promise.then(pdf => {
                // 循环渲染所有页面
                for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
                    pdf.getPage(pageNum).then(page => {
                        const scale = 1.5;
                        const viewport = page.getViewport({scale: scale});
                        const canvas = document.createElement('canvas');
                        const context = canvas.getContext('2d');
                        canvas.height = viewport.height;
                        canvas.width = viewport.width;
                        // 创建页面容器
                        const pageDiv = document.createElement('div');
                        pageDiv.className = 'pdf-page';
                        pageDiv.style.margin = '10px 0';
                        container.appendChild(pageDiv);
                        // 渲染到Canvas
                        page.render({
                            canvasContext: context,
                            viewport: viewport
                        }).promise.then(() => {
                            pageDiv.appendChild(canvas);
                        });
                    });
                }
            });
        }
        // 打印函数
        window.printPdf = function() {
            let base64Image = window.currentPDF;
            const printWindow = window.open('', '_blank');
            printWindow.document.write(`
                                        <html>
                                          <head>
                                            <title>电子面单打印</title>
                                            <style>
                                              body { margin: 0; padding: 20px }
                                              canvas {
                                                width: 100%!important;
                                                height: auto!important;
                                                page-break-after: always;
                                              }
                                            </style>
                                          </head>
                                          <body>
                                            <iframe src="data:application/pdf;base64,`+base64Image+`" width="100%" height="100%"></iframe>
                                          </body>
                                        </html>
                                      `);
            printWindow.document.close();
            printWindow.onload = function() {
                printWindow.print();
                printWindow.close();
            }
        }
        // function deliverPdfGoods(id) {
        //     febs.get(ctx + 'admin/order/deliverPdfGoods/' + id, null, function (e) {
        //         if(e.code == 200){
        //             console.info(e.data);
        //             console.info(e.data.pdfStream);
        //             console.info(e.data.result);
        //             layer.open({
        //                 type: 1,
        //                 title: "电子面单",
        //                 skin: 'layui-layer-rim', //加上边框
        //                 area: ['100%', '100%'], //宽高
        //                 shadeClose: true, //开启遮罩关闭
        //                 end: function (index, layero) {
        //                     return false;
        //                 },
        //                 content: '<div class="layui-card-body" id="pdfViewer"></div>'
        //             });
        //             if(e.data.pdfStream === null){
        //
        //                 renderPdf(e.data.pdfStreamList);
        //             }
        //             renderPdf(e.data.pdfStream);
        //         }else{
        //             febs.alert.warn(e.message);
        //         }
        //         $query.click();
        //     });
        // }
        // 渲染 PDF 到页面
        // function renderPdf(base64Data) {
        //     // Base64 转 Blob
        //     const byteCharacters = atob(base64Data);
        //     const byteNumbers = new Array(byteCharacters.length);
        //     for (let i = 0; i < byteCharacters.length; i++) {
        //         byteNumbers[i] = byteCharacters.charCodeAt(i);
        //     }
        //     const byteArray = new Uint8Array(byteNumbers);
        //     const blob = new Blob([byteArray], {type: 'application/pdf'});
        //
        //     // 生成临时 URL
        //     const url = URL.createObjectURL(blob);
        //
        //     // 使用 PDF.js 渲染
        //     pdfjsLib.getDocument(url).promise.then(function(pdf) {
        //         pdf.getPage(1).then(function(page) {
        //             const scale = 1.5;
        //             const viewport = page.getViewport({scale: scale});
        //             const canvas = document.createElement('canvas');
        //             const context = canvas.getContext('2d');
        //             canvas.height = viewport.height;
        //             canvas.width = viewport.width;
        //
        //             // 将 PDF 页面渲染到 Canvas
        //             page.render({
        //                 canvasContext: context,
        //                 viewport: viewport
        //             }).promise.then(function() {
        //                 $('#pdfViewer').html(canvas);
        //             });
        //         });
        //     }).catch(function(error) {
        //         layer.msg('PDF渲染失败: ' + error.message, {icon: 2});
        //     });
        // }
        // 查询按钮
        $query.on('click', function () {
@@ -456,11 +649,13 @@
                        {title: '操作',
                            templet: function (d) {
                                if(d.status === 2){
                                    return '<button class="layui-btn layui-btn-normal layui-btn-xs" lay-event="seeOrder" shiro:hasPermission="user:update">详情</button>'
                                    +'<button class="layui-btn layui-btn-normal layui-btn-xs" lay-event="deliverGoods" shiro:hasPermission="user:update">发货</button>'
                                    return '<button class="layui-btn layui-btn-normal layui-btn-xs" lay-event="deliverPdfGoods" shiro:hasPermission="user:update">一键发货</button>'
                                    +'<button class="layui-btn layui-btn-normal layui-btn-xs" lay-event="seeOrder" shiro:hasPermission="user:update">详情</button>'
                                    // +'<button class="layui-btn layui-btn-normal layui-btn-xs" lay-event="deliverGoods" shiro:hasPermission="user:update">发货</button>'
                                }else if(d.status === 3){
                                    return '<button class="layui-btn layui-btn-normal layui-btn-xs" lay-event="seeOrder" shiro:hasPermission="user:update">详情</button>'
                                    +'<button class="layui-btn layui-btn-normal layui-btn-xs" lay-event="updateDeliver" shiro:hasPermission="user:update">修改物流信息</button>'
                                        +'<button class="layui-btn layui-btn-normal layui-btn-xs" lay-event="cancelDeliver" shiro:hasPermission="user:update">取消发货</button>'
                                    // +'<button class="layui-btn layui-btn-normal layui-btn-xs" lay-event="updateDeliver" shiro:hasPermission="user:update">修改物流信息</button>'
                                }else{
                                    return '<button class="layui-btn layui-btn-normal layui-btn-xs" lay-event="seeOrder" shiro:hasPermission="user:update">详情</button>'
                                }
src/main/resources/templates/febs/views/modules/system/sender.html
New file
@@ -0,0 +1,97 @@
<div class="layui-fluid layui-anim febs-anim" id="sender-setting" lay-title="发货地址">
    <div class="layui-row layui-col-space8 febs-container">
        <form class="layui-form" action="" lay-filter="sender-setting-form">
            <div class="layui-card">
                <div class="layui-card-body flex" id="cardBodySender">
                </div>
                <div class="layui-card-footer">
                    <button class="layui-btn layui-btn-normal save-btn" lay-submit="" lay-filter="sender-setting-form-submit" id="submit">保存</button>
                </div>
            </div>
        </form>
    </div>
</div>
<style>
    .layui-form-label {
        width: 120px;
    }
    .layui-form-item .layui-input-block {
        margin-left: 150px;
    }
    .layui-table-form .layui-form-item {
        margin-bottom: 20px !important;
    }
    .flex{
        display: flex;
        flex-wrap: wrap;
    }
    .save-btn{
        display: block;
        margin: 0 auto;
    }
    .layui-upload-list{
        height: 200px;
    }
</style>
<script type="text/html" id="senderOperate">
    {{#  layui.each(d, function(index, item){ }}
    <div class="layui-form-item">
        <label class="layui-form-label febs-form-item-require">{{item.description}}:</label>
        <input type="text" lay-verify="required" name="{{item.code}}"
               autoComplete="off" value="{{item.value}}" class="layui-input">
    </div>
    {{# }) }}
</script>
<script data-th-inline="javascript" type="text/javascript">
    layui.use(['dropdown', 'jquery', 'validate', 'febs', 'form', 'eleTree', 'laytpl', 'upload'], function () {
        var $ = layui.jquery,
            febs = layui.febs,
            form = layui.form,
            validate = layui.validate
            , templateHtml = senderOperate.innerHTML
            , $cardBodySender = $("#cardBodySender")
            , laytpl = layui.laytpl
            , upload = layui.upload
            , $view = $('#sender-setting');
        form.verify(validate);
        form.render();
        dicDataReq("SENDER_SET");
        function dicDataReq(type) {
            $cardBodySender.empty();
            $.get(ctx + 'admin/common/findDicByType/' + type, function (r) {
                if (r.code === 200) {
                    var data = r.data;
                    laytpl(templateHtml).render(data, function(html) {
                        $cardBodySender.append(html);
                    })
                }
            });
        }
        form.on('submit(sender-setting-form-submit)', function (data) {
            $.ajax({
                'url':ctx + 'admin/system/bonusSystemSetting',
                'type':'post',
                'dataType':'json',
                'headers' : {'Content-Type' : 'application/json;charset=utf-8'},
                'traditional': true,
                'data':JSON.stringify(data.field),
                'success':function (data) {
                    if (data.code == 200) {
                        febs.alert.success(data.message);
                    }
                },
                'error':function () {
                    febs.alert.warn('服务器繁忙');
                }
            })
            return false;
        });
    });
</script>
src/main/resources/templates/index.html
@@ -25,7 +25,7 @@
<!--    <script src="https://unpkg.com/@wangeditor/editor@latest/dist/index.js"></script>-->
    <script src="http://gosspublic.alicdn.com/aliyun-oss-sdk-6.17.0.min.js"></script>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/wangeditor@latest/dist/wangEditor.min.js" ></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.6.347/pdf.min.js"></script>
    <style type="text/css">
        ::-webkit-scrollbar {
            height: 20px !important;
src/test/java/cc/mrbird/febs/AgentTest.java
@@ -1,127 +1,26 @@
package cc.mrbird.febs;
import cc.mrbird.febs.common.enumerates.FlowTypeEnum;
import cc.mrbird.febs.common.enumerates.MoneyFlowTypeEnum;
import cc.mrbird.febs.mall.entity.*;
import cc.mrbird.febs.mall.mapper.*;
import cc.mrbird.febs.mall.service.IMallMoneyFlowService;
import cc.mrbird.febs.pay.model.RefundStatus;
import cc.mrbird.febs.pay.util.FiuuRefundUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import cc.mrbird.febs.mall.dto.ApiCheckTraceInfoDto;
import cn.hutool.json.JSONUtil;
import com.best.javaSdk.ClientParamEnum;
import com.best.javaSdk.ClientParamService;
import com.best.javaSdk.kdTraceQuery.response.KdTraceQueryRsp;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@Slf4j
@SpringBootTest
public class AgentTest {
    @Autowired
    private MallActivityMapper mallActivityMapper;
    @Autowired
    private MallRefundMapper mallRefundMapper;
    @Autowired
    private MallOrderInfoMapper mallOrderInfoMapper;
    @Autowired
    private MallOrderItemMapper mallOrderItemMapper;
    @Autowired
    private MallGoodsSkuMapper mallGoodsSkuMapper;
    @Autowired
    private MallGoodsMapper mallGoodsMapper;
    @Autowired
    private FiuuRefundUtil fiuuRefundUtil;
    @Autowired
    private IMallMoneyFlowService mallMoneyFlowService;
    @Test
    public void refundJob() {
        LambdaQueryWrapper<MallRefundEntity> mallOrderRefundLambdaQueryWrapper = new LambdaQueryWrapper<>();
        mallOrderRefundLambdaQueryWrapper.eq(MallRefundEntity::getState, 3);
        mallOrderRefundLambdaQueryWrapper.eq(MallRefundEntity::getType, 1);
        List<MallRefundEntity> mallRefundEntities = mallRefundMapper.selectList(mallOrderRefundLambdaQueryWrapper);
        if(CollUtil.isEmpty(mallRefundEntities)){
            return;
        }
        mallRefundEntities.forEach(mallRefundEntity -> {
            processRefund(mallRefundEntity);
        });
        ApiCheckTraceInfoDto apiCheckTraceInfoDto = new ApiCheckTraceInfoDto();
        apiCheckTraceInfoDto.setTraceNo("60850712015414");
        KdTraceQueryRsp traceQueryRsp = ClientParamService.getInstance(ClientParamEnum.PRD.name()).checkTraceInfo(apiCheckTraceInfoDto);
        log.info("traceQueryRsp:{}", JSONUtil.parseObj(traceQueryRsp));
    }
    private void processRefund(MallRefundEntity mallRefundEntity) {
        try {
            MallOrderInfo mallOrderInfo = mallOrderInfoMapper.selectById(mallRefundEntity.getOrderId());
            MallOrderItem mallOrderItem = mallOrderItemMapper.selectById(mallRefundEntity.getItemId());
            MallGoodsSku mallGoodsSku = mallGoodsSkuMapper.selectById(mallOrderItem.getSkuId());
            String txnId = mallOrderInfo.getPayOrderNo();
            RefundStatus status = fiuuRefundUtil.pollRefundStatus(txnId);
            switch (status.getStatus()) {
                case "success":
                    updateOnSuccess(mallRefundEntity, mallOrderInfo, mallOrderItem, mallGoodsSku);
                    break;
                case "rejected":
                    updateOnRejected(mallRefundEntity, mallOrderItem);
                    break;
                default:
                    log.warn("未知状态:{}", status.getStatus());
            }
        } catch (Exception e) {
            log.error("处理退款失败: {}", e.getMessage(), e);
        }
    }
    private void updateOnSuccess(MallRefundEntity mallRefundEntity, MallOrderInfo mallOrderInfo, MallOrderItem mallOrderItem, MallGoodsSku mallGoodsSku) {
        mallOrderItem.setState(3);
        mallOrderItemMapper.updateById(mallOrderItem);
        MallGoods mallGoods = mallGoodsMapper.selectById(mallOrderItem.getGoodsId());
        mallGoods.setStock(mallGoods.getStock() + mallOrderItem.getCnt());
        mallGoods.setVolume(mallGoods.getVolume() - mallOrderItem.getCnt());
        mallGoodsMapper.updateById(mallGoods);
        mallGoodsSku.setStock(mallGoodsSku.getStock() + mallOrderItem.getCnt());
        mallGoodsSku.setSkuVolume(mallGoodsSku.getSkuVolume() - mallOrderItem.getCnt());
        mallGoodsSkuMapper.updateById(mallGoodsSku);
        mallRefundEntity.setState(1);
        mallRefundEntity.setUpdatedTime(DateUtil.date());
        mallRefundMapper.updateById(mallRefundEntity);
        mallMoneyFlowService.addMoneyFlow(
                mallOrderInfo.getMemberId(),
                mallRefundEntity.getAmount(),
                MoneyFlowTypeEnum.WECHAT_REFUND.getValue(),
                mallOrderInfo.getOrderNo(),
                FlowTypeEnum.WECHAT.getValue(),
                "FIUU退款",
                2);
        List<MallOrderItem> mallOrderItemList = mallOrderItemMapper.selectListByNotInStateAndOrderId(3, mallRefundEntity.getOrderId());
        if (CollUtil.isEmpty(mallOrderItemList)) {
            MallOrderInfo mallOrderRefund = mallOrderInfoMapper.selectById(mallRefundEntity.getOrderId());
            mallOrderRefund.setStatus(6);
            mallOrderInfoMapper.updateById(mallOrderRefund);
        }
    }
    private void updateOnRejected(MallRefundEntity mallRefundEntity, MallOrderItem mallOrderItem) {
        mallOrderItem.setState(1);
        mallOrderItemMapper.updateById(mallOrderItem);
        mallRefundEntity.setState(2);
        mallRefundMapper.updateById(mallRefundEntity);
    }
}