package com.sdtool.common.base;

import com.sdtool.common.api.*;
import com.sdtool.common.datas.ERPModule;
import com.sdtool.common.datas.HttpCode;
import com.sdtool.common.entity.base.BaseEntity;
import com.sdtool.common.entity.base.InvokeCallParams;
import com.sdtool.common.entity.base.InvokeCallResult;
import com.sdtool.common.entity.base.ScriptContent;
import com.sdtool.common.entity.doc.BusinessScript;
import com.sdtool.common.entity.site.ERPTokenUser;
import com.sdtool.common.entity.system.KeyValuePair;
import com.sdtool.common.entity.system.RedisHLockInfo;
import com.sdtool.common.entity.system.RedisHLockResult;
import com.sdtool.common.tool.ERPUtils;
import com.sdtool.common.tool.PageFlipperCommand;
import com.dySweetFishPlugin.elasticsearch.ESClient;
import com.dySweetFishPlugin.redis.RedisService;
import com.dySweetFishPlugin.sql.dao.OperatorWait;
import com.dySweetFishPlugin.tool.lang.Holder;
import com.dySweetFishPlugin.tool.lang.ThreadFactoryWithNamePrefix;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.sweetfish.convert.json.JsonConvert;
import com.sweetfish.service.AbstractService;
import com.sweetfish.service.Local;
import com.sweetfish.service.RetResult;
import com.sweetfish.source.PageFlipper;
import com.sweetfish.util.AnyValue;
import com.sweetfish.util.AutoLoad;
import com.sweetfish.util.ResourceFactory;
import groovy.lang.GroovyClassLoader;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.rex.RMap;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 业务接口基类
 * Created by Administrator on 2017-03-21.
 */
@AutoLoad(false)
public abstract class BaseService extends AbstractService {

    @Resource(name = "server")
    protected ResourceFactory rf;

    @Resource(name = "APP_HOME")
    protected String appHome;

    @Resource
    private DocInfoService docInfoService;

    @Resource
    private SupplierInitService supplierService;

    @Resource
    private ERPLockDataService lockDataService;

    @Resource
    private SystemService systemService;

    @Resource
    private RedisService redisService;

    @Resource
    private PermissionService permissionService;

    @Resource
    private JsonConvert jsonConvert;

    /**
     * 业务代码的脚本目录
     */
    protected String businessScriptRoot;

    /**
     * 业务执行脚本接口缓存
     */
    @SuppressWarnings("rawtypes")
    private Cache<String, BusinessExecutor> businessExecCache;

    private ExecutorService scriptExecutor;

    /**
     * Groovy脚本类加载器，为什么每一个Service都有一套，方便微服务化，避免远程调用时找不到对象
     */
    protected ConcurrentHashMap<String, GroovyClassLoader> groovyClMap = new ConcurrentHashMap<>();

    private boolean isUseScriptService;

    private boolean checkClassConstructor(Class<?> clazz) {
        Constructor<?>[] constructors = clazz.getDeclaredConstructors();
        boolean hasConstructor = false;

        for (Constructor<?> constructor : constructors) {
            Class<?>[] paramTypes = constructor.getParameterTypes();
            if (paramTypes.length == 2 && paramTypes[0] == String.class && paramTypes[1] == long.class) {
                hasConstructor = true;
                break;
            }
        }
        return hasConstructor;
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    private GroovyClassLoader getGroovyClassLoader(long supplierCode) {
        //这里没有直接groovyClMap.get的原因是saas情况下，动态增加数据源，由于没有初始化ClassLoader，会导致取不到，所以使用computeIfAbsent使用
        return groovyClMap.computeIfAbsent(String.valueOf(supplierCode), k -> {
            CompilerConfiguration groovyConfig = ERPUtils.buildERPGroovyConfig();
            ClassLoader cl = this.getClass().getClassLoader();

            GroovyClassLoader classLoader = new GroovyClassLoader(cl, groovyConfig);

            String scriptFile = businessScriptRoot + File.separator + 0 + File.separator + "business" + File.separator + "BE_Init_ExpandoGroovy.groovy";

            Path x = Paths.get(scriptFile);
            String code = FilenameUtils.getBaseName(x.toFile().getName());
            if (code.startsWith("BE_")) {
                try {
                    code = code.substring(3);
                    String scriptDirectory = x.getParent().toString();

                    classLoader.addClasspath(businessScriptRoot + File.separator + 0 + File.separator + "commons");
                    classLoader.addClasspath(scriptDirectory);

                    //添加包名
                    String fileCode = readUTF8File(x, false);

                    Class groovyClass = classLoader.parseClass(fileCode, "BE_" + code + "_" + supplierCode + ".groovy");

                    try {
                        BusinessExecutor p;
                        if (checkClassConstructor(groovyClass)) {
                            p = (BusinessExecutor) groovyClass.getDeclaredConstructor(String.class, long.class).newInstance(appHome, supplierCode);
                        } else {
                            p = (BusinessExecutor) groovyClass.getDeclaredConstructor().newInstance();
                        }

                        //注入，方便服务使用其他服务
                        if (rf != null) {
                            rf.inject(p);
                        } else {
                            ResourceFactory.root().inject(p);
                        }

                        p.start(supplierCode);

                        businessExecCache.put(getScriptCodeKey(0, code, supplierCode), p);
                        logger.info("系统启动-载入" + supplierCode + "-" + code + "脚本");
                    } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
                             NoSuchMethodException e) {
                        logger.error(e.getMessage(), e);
                    }

                } catch (CompilationFailedException | ClassCastException e) {
                    logger.error(e.getMessage(), e);
                }

            }


            return classLoader;
        });
    }

    /**
     * 该方法必须是可以重复调用， 当reload时需要重复调用init方法
     * 远程模式下该方法会重载成空方法
     *
     * @param config 配置参数
     */
    @Override
    @SuppressWarnings({"rawtypes", "unchecked"})
    public void init(AnyValue config) {
        int minExecutorCount = 4;
        int maxExecutorCount = 16;
        String executorName = "[" + this.getClass().getSimpleName() + "线程池" + "]";
        AnyValue executorValue = config.getAnyValue("executor");
        if (executorValue != null) {
            minExecutorCount = executorValue.getIntValue("min");
            maxExecutorCount = executorValue.getIntValue("max");
            executorName = executorValue.getValue("name");
        }
        scriptExecutor = new ThreadPoolExecutor(
                minExecutorCount,
                maxExecutorCount,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(Integer.MAX_VALUE),
                new ThreadFactoryWithNamePrefix(executorName));


        isUseScriptService = false;
        AnyValue isUseScriptValue = config.getAnyValue("isUseScriptService");
        if (isUseScriptValue != null) {
            isUseScriptService = isUseScriptValue.getBoolValue("value");
        }

        if (isUseScriptService) {
            AnyValue root2 = config.getAnyValue("BusinessScriptRoot");
            if (root2 != null) {
                businessScriptRoot = appHome + "/" + root2.getValue("value");
            } else {
                businessScriptRoot = appHome + "/" + "conf/script";
            }
//            businessExecCache = new ConcurrentHashMap<>();
            businessExecCache = CacheBuilder.newBuilder().expireAfterWrite(365, TimeUnit.DAYS).build();

            CompilerConfiguration groovyConfig = ERPUtils.buildERPGroovyConfig();
            ClassLoader cl = this.getClass().getClassLoader();

            List<KeyValuePair> dataItemList = supplierService.getDataBaseItemList();

            //添加一个公共的ClassLoader
            dataItemList.add(0, new KeyValuePair("公共", "0"));
            dataItemList.forEach((x) -> groovyClMap.put(x.getValue(), new GroovyClassLoader(cl, groovyConfig)));

            List<String> preCompilerFiles = new ArrayList<>();
            //2024-10-05 tt 能预加载的脚本一定是公共脚本，但BE_Init_ExpandoGroovy这个需要每一个supplierCode都有一个
            //强行注入Groovy扩展方法，担心多线程执行脚本，脚本里面扩展方法有问题，启动Service的时候注入算了
            //通过BE_System_TestListExt脚本测试成功

            preCompilerFiles.add(businessScriptRoot + File.separator + 0 + File.separator + "business" + File.separator + "BE_Init_ExpandoGroovy.groovy");

            for (AnyValue scriptValue : config.getAnyValues("preCompiler")) {
                preCompilerFiles.add(businessScriptRoot + File.separator + scriptValue.getValue("value"));
            }

            for (String scriptFile : preCompilerFiles) {
                Path x = Paths.get(scriptFile);
                String code = FilenameUtils.getBaseName(x.toFile().getName());
                if (code.startsWith("BE_")) {
                    try {
                        code = code.substring(3);
                        String scriptDirectory = x.getParent().toString();
                        String fileCode = readUTF8File(x, false);

                        if (StringUtils.equalsIgnoreCase(code, "Init_ExpandoGroovy")) {
                            String finalCode = code;
                            dataItemList.forEach((dataItem) -> {
                                //给Groovy增加通用目录方便点
                                getGroovyClassLoader(Long.parseLong(dataItem.getValue())).addClasspath(businessScriptRoot + File.separator + 0 + File.separator + "commons");
                                getGroovyClassLoader(Long.parseLong(dataItem.getValue())).addClasspath(scriptDirectory);

                                Class groovyClass = getGroovyClassLoader(Long.parseLong(dataItem.getValue())).parseClass(fileCode, "BE_" + finalCode + "_" + dataItem.getValue() + ".groovy");

                                try {
                                    BusinessExecutor p;
                                    if (checkClassConstructor(groovyClass)) {
                                        p = (BusinessExecutor) groovyClass.getDeclaredConstructor(String.class, long.class).newInstance(appHome, Long.parseLong(dataItem.getValue()));
                                    } else {
                                        p = (BusinessExecutor) groovyClass.getDeclaredConstructor().newInstance();
                                    }

                                    //注入，方便服务使用其他服务
                                    if (rf != null) {
                                        rf.inject(p);
                                    } else {
                                        ResourceFactory.root().inject(p);
                                    }

                                    p.start(Long.parseLong(dataItem.getValue()));

                                    businessExecCache.put(getScriptCodeKey(0, finalCode, Long.parseLong(dataItem.getValue())), p);
                                    logger.info("系统启动-载入" + dataItem.getValue() + "-" + finalCode + "脚本:" + this.getClass().getSimpleName());
                                } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
                                         NoSuchMethodException e) {
                                    logger.error(e.getMessage(), e);
                                }
                            });
                        } else {
                            String supplierCode = x.toFile().getParentFile().getParentFile().getName();
                            //给Groovy增加通用目录方便点
                            getGroovyClassLoader(Long.parseLong(supplierCode)).addClasspath(businessScriptRoot + File.separator + 0 + File.separator + "commons");
                            getGroovyClassLoader(Long.parseLong(supplierCode)).addClasspath(scriptDirectory);

                            Class groovyClass = getGroovyClassLoader(Long.parseLong(supplierCode)).parseClass(fileCode, "BE_" + code + "_" + Long.parseLong(supplierCode) + ".groovy");

                            try {
                                BusinessExecutor p;
                                if (checkClassConstructor(groovyClass)) {
                                    p = (BusinessExecutor) groovyClass.getDeclaredConstructor(String.class, long.class).newInstance(appHome, Long.parseLong(supplierCode));
                                } else {
                                    p = (BusinessExecutor) groovyClass.getDeclaredConstructor().newInstance();
                                }

                                //注入，方便服务使用其他服务
                                if (rf != null) {
                                    rf.inject(p);
                                } else {
                                    ResourceFactory.root().inject(p);
                                }

                                p.start(Long.parseLong(supplierCode));

                                businessExecCache.put(getScriptCodeKey(1, code, Long.parseLong(supplierCode)), p);
                                logger.info("系统启动-载入" + supplierCode + "-" + code + "脚本");
                            } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
                                     NoSuchMethodException e) {
                                logger.error(e.getMessage(), e);
                            }
                        }


                    } catch (CompilationFailedException | ClassCastException e) {
                        logger.error(e.getMessage(), e);
                    }

                }
            }
        }
    }

    /**
     * 进程退出时，调用Service销毁
     * 远程模式下该方法会重载成空方法
     *
     * @param config 配置参数
     */
    @Override
    public void destroy(AnyValue config) {
        if (isUseScriptService) {
            businessExecCache.asMap().forEach((key, value) -> {
                String supplierCode = key.split("-")[0];
                value.destroy(Long.parseLong(supplierCode));
            });
            groovyClMap.forEach((k, v) -> v.clearCache());
            businessExecCache.invalidateAll();
            businessExecCache.cleanUp();
        }
    }


    /**
     * 从数据库分页查询获取全部数据，然后处理
     *
     * @param pageSize 每页数据
     * @param command  处理接口
     */
    @SuppressWarnings("SameParameterValue")
    protected void execPageFlipper(int pageSize, PageFlipperCommand command) {
        PageFlipper pageFlipper = new PageFlipper(1, pageSize);

        command.exec(pageFlipper);

        int iPageTotal = pageFlipper.getPageTotal();
        for (int i = 2; i <= iPageTotal; i++) {
            pageFlipper.setPageIndex(i);
            command.exec(pageFlipper);
        }
    }

    /**
     * 读取utf8文件到字符串
     *
     * @param scriptFile 物理文件
     * @param trimed     格式化
     * @return 文件内容
     */
    @SuppressWarnings("SameParameterValue")
    protected String readUTF8File(Path scriptFile, boolean trimed) {
        final StringBuilder buf = new StringBuilder();

        try {
            Integer iIndex = 0;
            Holder<Integer> prefix = Holder.of(iIndex);
            try (Stream<String> lines = Files.lines(scriptFile, StandardCharsets.UTF_8)) {
                lines.forEach((line) -> {
                    line = (trimed) ? line.trim() : line;
                    if (!line.isEmpty()) {
                        if (prefix.get() == 0) {
                            buf.append((trimed) ? line.trim() : line);
                        } else {
                            buf.append("\n").append((trimed) ? line.trim() : line);
                        }

                        prefix.set(1);
                    } else {
                        if (!trimed) buf.append("\n");
                    }
                });
            }

        } catch (IOException e) {
            logger.error(e.getMessage(), e);
        }

        return buf.toString();
    }

    protected <T> List<T> getRedisDocInfos(String redisKey, Class<T> resultClazz) {
        Map<String, String> docMap = redisService.hgetAll(redisKey);
        List<T> result = new ArrayList<>();
        docMap.forEach((key, value) -> result.add(jsonConvert.convertFrom(resultClazz, value)));

        return result;
    }

    protected <T> List<T> getRedisDocInfos(Class<T> resultClazz, String redisKey, String... fields) {
        List<T> result = new ArrayList<>();
        if (fields.length > 0) {
            List<String> docList = redisService.hmget(redisKey, fields);
            docList = docList.stream().filter(StringUtils::isNotBlank).collect(Collectors.toList());

            docList.forEach(value -> result.add(jsonConvert.convertFrom(resultClazz, value)));
        }
        return result;
    }

    protected <T> T getRedisDocInfo(String redisKey, String field, Class<T> clazz) {
        String docJson = redisService.hget(redisKey, field);
        if (StringUtils.isNotBlank(docJson)) {
            return jsonConvert.convertFrom(clazz, docJson);
        } else return null;
    }

    protected String getScriptCodeKey(int commonFlag, String code, long supplierCode) {
        if (commonFlag == 1) {
            return 0 + "-" + code;
        } else {
            return supplierCode + "-" + code;
        }
    }

    /**
     * 检查脚本文件是否存在
     * 避免多次初始化
     * 2024-10-05 tt 检查了一下使用地方，均为独立脚本，不增加参数调整了
     *
     * @param erpModule    ERPModule模块
     * @param code         业务编号
     * @param supplierCode 分表
     * @return 文件是否存在
     */
    @SuppressWarnings("unused")
    protected boolean checkScriptIdExists(ERPModule erpModule, String code, long supplierCode) {
        String fileName = businessScriptRoot + File.separator +
                supplierCode + File.separator +
                erpModule.getSaveDirectory() + File.separator +
                "BE_" + erpModule.getScriptPrefix() + code + ".groovy";
        File scriptFile = new File(fileName);
        return scriptFile.exists();
    }

    /**
     * 从模板创建新的业务脚本并保存
     *
     * @param scriptTemplateFunction 模板参数构建方法
     * @param templateName           模板文件名，必须存在conf目录下
     * @param erpModule              ERPModule模块
     * @param id                     保存的对象id
     * @param dataSourceId           分库
     * @param supplierCode           分表
     * @param scriptName             脚本名称
     * @param currentUser            操作用户
     */
    @SuppressWarnings("SameParameterValue")
    protected void saveAddScriptIdContent(Function<String, Map<String, Object>> scriptTemplateFunction,
                                          String templateName,
                                          ERPModule erpModule,
                                          long id,
                                          String dataSourceId, long supplierCode,
                                          String scriptName,
                                          int commonFlag,
                                          ERPTokenUser currentUser) {
        String scriptBody = ERPUtils.parseScriptFromTemplate(scriptTemplateFunction, appHome + File.separator + "conf", templateName);

        ScriptContent scriptContent = ScriptContent.newBuilder()
                .code(erpModule.getScriptPrefix() + id)
                .id(id)
                .module(erpModule.getValue())
                .scriptBody(scriptBody)
                .commonFlag(commonFlag)
                .build();

        if (docInfoService.saveBusinessScriptContent(scriptContent, false, dataSourceId, supplierCode, currentUser) == 1) {
            BusinessScript businessScript = BusinessScript.newBuilder()
                    .businessName(scriptName)
                    .businessCode(erpModule.getScriptPrefix() + id)
                    .businessModule(erpModule.getValue())
                    .systemFlag(1)
                    .commonFlag(commonFlag)
                    .build();
            docInfoService.addBusinessScriptLocal(businessScript, currentUser, dataSourceId, supplierCode);
        }

        invalidateScriptCache(supplierCode + "-" + erpModule.getScriptPrefix() + id);
    }

    /**
     * 获取业务脚本接口
     *
     * @param code 业务代码
     * @return BusinessExecutor实例
     */
    @SuppressWarnings({"rawtypes", "unused", "unchecked"})
    private BusinessExecutor loadBusinessExecutor(String code, ERPModule erpModule, String dataSourceId, long supplierCode) {
        if (!isUseScriptService) {
            return null;
        }

        try {
            BusinessScript businessScript = docInfoService.getRedisBusinessScript(code, supplierCode);
            int commonFlag = (businessScript == null) ? 0 : businessScript.getCommonFlag();
            return businessExecCache.get(getScriptCodeKey(commonFlag, code, supplierCode), () -> {

                String scriptDirectory;
                long localSupplierCode = supplierCode;

                if (businessScript == null) {
                    //未记录到redis中的脚本，也需要执行，有可能是开发人员手动添加的脚本，但未重启系统
                    //2024-10-05 新加的脚本统一定义为私有脚本，通过admin执行的只允许为数据修复脚本脚本?待定，先用NONe
                    scriptDirectory = businessScriptRoot + File.separator +
                            localSupplierCode + File.separator +
                            erpModule.getSaveDirectory();
                } else {
                    if (businessScript.getCommonFlag() == 1) {
                        localSupplierCode = 0L;
                    }
                    scriptDirectory = businessScriptRoot + File.separator +
                            localSupplierCode + File.separator +
                            ERPModule.getModule(businessScript.getBusinessModule()).getSaveDirectory();
                }

                getGroovyClassLoader(localSupplierCode).addClasspath(businessScriptRoot + File.separator + 0 + File.separator + "commons");
                getGroovyClassLoader(localSupplierCode).addClasspath(scriptDirectory);

                //原始脚本文件
                String prefix = "BE_";

                Path scriptPath = Paths.get(scriptDirectory + File.separator + prefix + code + ".groovy");

                if (!Files.exists(scriptPath)) {
                    return null;
                }

                String fileStr = readUTF8File(scriptPath, false);

                Class groovyClass = getGroovyClassLoader(localSupplierCode).parseClass(fileStr, "BE_" + code + "_" + localSupplierCode + ".groovy");

                //下面这句话为什么加上呢？感觉有问题啊，每次载入脚本都清除之前的缓存？
                //getGroovyClassLoader(supplierCode).clearCache();

                BusinessExecutor p;
                if (checkClassConstructor(groovyClass)) {
                    p = (BusinessExecutor) groovyClass.getDeclaredConstructor(String.class, long.class).newInstance(appHome, supplierCode);
                } else {
                    p = (BusinessExecutor) groovyClass.getDeclaredConstructor().newInstance();
                }

                //注入，方便服务使用其他服务
                if (rf != null) {
                    rf.inject(p);
                } else {
                    ResourceFactory.root().inject(p);
                }

                //注意公共脚本不要读取和supplierCode相关的数据
                p.start(supplierCode);

                logger.info(code + "脚本重新编译了！！！！！！！！！！！！");
//                redisService.hset(keysService.getRedisKey(RedisKeys.KEY_SCRIPTCOMPILE, supplierCode), code, "1");
                return p;
            });
        } catch (Exception e) {
            logger.info(e.getMessage(), e);
            return null;
        }
    }

    @SuppressWarnings({"unchecked", "unused"})
    public <S extends BaseEntity, R extends BaseEntity> boolean checkInScript(String businessCode,
                                                                              ERPModule erpModule,
                                                                              String dataSourceId,
                                                                              long supplierCode,
                                                                              Supplier<S> supplier) {
        try {
            BusinessExecutor<S, R> businessExecutor = loadBusinessExecutor(businessCode, erpModule, dataSourceId, supplierCode);
            if (businessExecutor == null) {
                return true;
            }
            return businessExecutor.checkIn(supplier.get());
        } catch (Exception e) {
            logger.error("业务脚本 " + businessCode + "处理错误:" + e.getMessage(), e);
            return true;
        }
    }

    /**
     * 执行脚本的通用写法
     *
     * @param <S>          源参数类型
     * @param <R>          返回对象类型
     * @param businessCode 脚本编号
     * @param dataSourceId 分库
     * @param supplierCode 分表
     * @param supplier     传入参数的生成函数
     * @param consumer     finally块中消费下source
     * @return 执行结果
     */
    @SuppressWarnings({"unchecked", "unused"})
    public <S extends BaseEntity, R extends BaseEntity> RetResult<R> handleScript(String businessCode,
                                                                                  ERPModule erpModule,
                                                                                  String dataSourceId,
                                                                                  long supplierCode,
                                                                                  Supplier<S> supplier,
                                                                                  Consumer<S> consumer) {
        S source = supplier.get();

        try {
            BusinessExecutor<S, R> businessExecutor = loadBusinessExecutor(businessCode, erpModule, dataSourceId, supplierCode);
            if (businessExecutor == null) {
                return RetResult.<R>errorT().retcode(HttpCode.INTERNAL_SERVER_ERROR.value()).retinfo("业务脚本处理错误：脚本接口返回为空");
            }
            return businessExecutor.execute(source);
        } catch (Exception e) {
            logger.error("业务脚本 " + businessCode + "处理错误:" + e.getMessage(), e);
            //经过测试，脚本文件不存在，或者脚本存在错误，都会在此抛出异常
            return RetResult.<R>errorT().retcode(HttpCode.INTERNAL_SERVER_ERROR.value()).retinfo("业务脚本处理错误：" + e.getMessage());
        } finally {
            //https://blog.csdn.net/ShyTan/article/details/81711604
            if (consumer != null) {
                consumer.accept(source);
            }
        }
    }

    /**
     * 执行脚本的通用写法
     *
     * @param <S>          源参数类型
     * @param <R>          返回对象类型
     * @param businessCode 脚本编号
     * @param dataSourceId 分库
     * @param supplierCode 分表
     * @param supplier     传入参数的生成函数bu
     * @return 执行结果
     */
    @SuppressWarnings({"unused"})
    public <S extends BaseEntity, R extends BaseEntity> RetResult<R> handleScript(String businessCode,
                                                                                  ERPModule erpModule,
                                                                                  String dataSourceId,
                                                                                  long supplierCode,
                                                                                  Supplier<S> supplier) {
        return handleScript(businessCode, erpModule, dataSourceId, supplierCode, supplier, null);
    }

    /**
     * 非档案的业务统一调用方法
     * 目标：rest只有一个方法，service也只有一个方法
     * 1、如果数据检查放在脚本外执行，则无法实现service也只有一个方法
     * 2、如果java里面使用了脚本的检查方法，并返回一个强类型，同样也保证不了service也只有一个方法
     * 3、为了统一，前端统一使用body调用接口，传入参数只能是一个InvokeCallParams，并且返回值也只能是一个InvokeCallResult，而且脚本需要两次的json反序列化
     * 4、如果全部使用map，对于groovy来讲可以做弱类型的，但那样编码效率不高，获取对象属性无法智能感知
     * 如果使用java的强类型，
     *
     * @param businessCode 业务名
     * @param supplierCode 分表
     */
    @SuppressWarnings("unchecked")
    protected RetResult<InvokeCallResult> callScript(String businessCode, InvokeCallParams source, ERPTokenUser currentUser, String dataSourceId, long supplierCode) {
        BusinessScript businessScript = docInfoService.getRedisBusinessScript(businessCode, supplierCode);
        if (businessScript == null) {
            //此种方式调用，必须要在redis中存在脚本项目，不存在的情况还要一堆参数手动添加进去，这里就不在处理了。
            //动态执行一个新脚本，可以先在 业务脚本管理 中添加进去，然后前端调用即可
            //2024-01-23此处返回测试成功
            return RetResult.<InvokeCallResult>errorT().retinfo("业务脚本" + businessCode + "不存在，请添加业务脚本");
        }

        //这里和handleScript或者doIds之类的方法不同，之前的方法可以把脚本编译的工作全放在线程里面，这里则放在了主线程中，毕竟完全脚本化，需要同步执行脚本的check方法，暂时先这样
        BusinessExecutor<InvokeCallParams, InvokeCallResult> businessExecutor = loadBusinessExecutor(businessCode, ERPModule.getModule(businessScript.getBusinessModule()), dataSourceId, supplierCode);

        if (businessExecutor == null) {
            return RetResult.<InvokeCallResult>errorT().retcode(HttpCode.INTERNAL_SERVER_ERROR.value()).retinfo("业务脚本处理错误：脚本接口返回为空");
        }

        try {
            if (businessExecutor.needLogin(source)) {
                if (currentUser == null) {
                    return RetResult.<InvokeCallResult>errorT().retcode(HttpCode.FORBIDDEN.value()).retinfo("未登录");
                }
            }

            //检查token
            if (businessExecutor.needToken(source)) {
                if (StringUtils.isBlank(source.getToken())) {
                    return RetResult.<InvokeCallResult>errorT().retcode(HttpCode.BAD_REQUEST.value()).retinfo("参数token传入不正确");
                }

                int iCheck = systemService.checkFormToken(source.getToken(), supplierCode);

                if (iCheck == -2) {
                    return RetResult.<InvokeCallResult>errorT().retcode(HttpCode.CONFLICT.value()).retinfo("token无效，请关闭页面重新请求");
                }
                if (iCheck == -1) {
                    return RetResult.<InvokeCallResult>errorT().retcode(HttpCode.CONFLICT.value()).retinfo("订单已提交，请勿重复提交");
                }
            }

            //判断权限
            String perms = businessExecutor.getPerms(currentUser);
            if (StringUtils.isNotEmpty(perms)) {
                int perm = permissionService.ifPermission(currentUser, perms, null, dataSourceId, supplierCode);
                if (perm != 1) {
                    return RetResult.<InvokeCallResult>errorT().retcode(HttpCode.FORBIDDEN.value()).retinfo("无操作权限");
                }
            }

            //下面的代码一定要注意做好异常检查 try catch finally嵌套，catch会只执行内层，finally执行所有

            try {
                //检查数据，这里应该可以尝试一下合并check和before

                RetResult<InvokeCallParams> checkResult = businessExecutor.checkExecute(source);
                if (!checkResult.isSuccess()) {
                    return RetResult.<InvokeCallResult>errorT().retinfo(checkResult.getRetinfo());
                }

                try {
                    //开始执行前的函数，可用于 锁定数据 之类的处理
                    RetResult<InvokeCallParams> beforeResult = businessExecutor.beforeExecute(checkResult.getResult());
                    if (!beforeResult.isSuccess()) {
                        return RetResult.<InvokeCallResult>errorT().retinfo(beforeResult.getRetinfo());
                    }

                    //执行execute方法
                    OperatorWait aWait = businessExecutor.getAWait(beforeResult.getResult());
                    if (aWait == OperatorWait.SYNC) {
                        try {
                            RetResult<InvokeCallResult> scriptResult = businessExecutor.execute(beforeResult.getResult());
                            businessExecutor.afterExecute(!scriptResult.isSuccess(), beforeResult.getResult(), scriptResult);
                            return scriptResult;
                        } catch (Exception e3) {
                            //这里的异常为execute里面的错误，假定不是afterExecute里面的错误，afterExecute感觉很难出错，编码上注意点算了
                            //如果afterExecute执行出错了，反正也基本要手动处理业务数据
                            logger.error(e3.getMessage(), e3);
                            //这里由于在before中执行了锁定数据之类的操作，这里需要解锁 afterExecute里面的错误就暂时不管了
                            businessExecutor.afterExecute(true, beforeResult.getResult(), RetResult.<InvokeCallResult>errorT().retinfo(e3.getMessage()));
                            return RetResult.<InvokeCallResult>errorT().retcode(HttpCode.INTERNAL_SERVER_ERROR.value()).retinfo("业务" + businessCode + "执行execute错误：" + e3.getMessage());
                        }
                    } else {
                        CompletableFuture<RetResult<InvokeCallResult>> runFuture = CompletableFuture.supplyAsync(
                                () -> businessExecutor.execute(beforeResult.getResult()),
                                scriptExecutor);
                        if (aWait == OperatorWait.AWAIT) {
                            try {
                                //同样是假定afterExecute不出错
                                RetResult<InvokeCallResult> scriptResult = runFuture.get(20, TimeUnit.SECONDS);
                                //1123123
                                businessExecutor.afterExecute(!scriptResult.isSuccess(), beforeResult.getResult(), scriptResult);
                                return scriptResult;
                            } catch (InterruptedException | ExecutionException | TimeoutException e) {
                                logger.error(e.getMessage(), e);
                                businessExecutor.afterExecute(true, beforeResult.getResult(), RetResult.<InvokeCallResult>errorT().retinfo(e.getMessage()));
                                return RetResult.<InvokeCallResult>errorT().retcode(HttpCode.INTERNAL_SERVER_ERROR.value()).retinfo(e.getMessage());
                            }
                        } else {
                            runFuture.whenComplete((scriptResult, e) -> {
                                if (e == null) {
                                    businessExecutor.afterExecute(!scriptResult.isSuccess(), beforeResult.getResult(), scriptResult);
                                } else {
                                    businessExecutor.afterExecute(true, beforeResult.getResult(), RetResult.<InvokeCallResult>errorT().retinfo(e.getMessage()));
                                    logger.error(e.getMessage(), e);
                                }
                            });
                            return RetResult.<InvokeCallResult>successT().result(InvokeCallResult.success());
                        }
                    }
                } catch (Exception e2) {
                    //这里的异常为beforeExecute里面的错误，应该没有执行execute方法，无需执行afterExecute
                    logger.error(e2.getMessage(), e2);
                    return RetResult.<InvokeCallResult>errorT().retcode(HttpCode.INTERNAL_SERVER_ERROR.value()).retinfo("业务" + businessCode + "执行beforeExecute错误：" + e2.getMessage());
                }
            } catch (Exception e1) {
                //捕获check函数里面的错误
                logger.error(e1.getMessage(), e1);
                return RetResult.<InvokeCallResult>errorT().retcode(HttpCode.INTERNAL_SERVER_ERROR.value()).retinfo("业务" + businessCode + "执行checkExecute错误：" + e1.getMessage());
            }
        } finally {
            //检查token
            if (businessExecutor.needToken(source)) {
                if (StringUtils.isNotBlank(source.getToken())) {
                    systemService.resetFormToken(source.getToken(), supplierCode);
                }
            }
        }

    }

    /**
     * 执行脚本的通用写法
     *
     * @param <S>          参数
     * @param <R>          返回值
     * @param businessCode 脚本代码，一般去掉BE_
     * @param dataSourceId 分库
     * @param supplierCode 分表
     * @param params       参数
     * @return 返回执行结果
     */
    @SuppressWarnings({"unchecked", "unused"})
    private <S extends BaseEntity, R extends BaseEntity> RetResult<List<R>> handleScriptList(String businessCode,
                                                                                             ERPModule erpModule,
                                                                                             String dataSourceId,
                                                                                             long supplierCode,
                                                                                             S params) {
        try {
            BusinessExecutor<S, R> businessExecutor = loadBusinessExecutor(businessCode, erpModule, dataSourceId, supplierCode);
            if (businessExecutor == null) {
                return RetResult.error().retcode(HttpCode.INTERNAL_SERVER_ERROR.value()).retinfo("业务脚本处理错误：脚本接口返回为空");
            }
            return businessExecutor.executeList(params);
        } catch (Exception e) {
            logger.error("业务脚本 " + businessCode + "处理错误:" + e.getMessage(), e);
            //经过测试，脚本文件不存在，或者脚本存在错误，都会在此抛出异常
            return RetResult.error().retcode(HttpCode.INTERNAL_SERVER_ERROR.value()).retinfo("业务脚本处理错误：" + e.getMessage());
        }
    }

    /**
     * 执行脚本的通用写法
     *
     * @param <S>          参数
     * @param <R>          返回值
     * @param businessCode 脚本代码，一般去掉BE_
     * @param dataSourceId 分库
     * @param supplierCode 分表
     * @param supplier     参数方法
     * @return 返回执行结果
     */
    @SuppressWarnings({"unused"})
    public <S extends BaseEntity, R extends BaseEntity> RetResult<List<R>> handleScriptList(String businessCode,
                                                                                            ERPModule erpModule,
                                                                                            String dataSourceId,
                                                                                            long supplierCode,
                                                                                            Supplier<S> supplier) {
        return handleScriptList(businessCode, erpModule, dataSourceId, supplierCode, supplier.get());
    }

    @SuppressWarnings("rawtypes")
    public void invalidateScriptCache(String codeKey) {
        if (!isUseScriptService) {
            return;
        }

        BusinessExecutor p = businessExecCache.getIfPresent(codeKey);
        if (p != null) {
            String supplierCode = codeKey.split("-")[0];
            p.destroy(Long.parseLong(supplierCode));
            businessExecCache.invalidate(codeKey);
        } else {
            p = businessExecCache.getIfPresent(0 + "-" + codeKey.split("-")[1]);
            if (p != null) {
                String newCodeKey = 0 + "-" + codeKey.split("-")[1];
                String supplierCode = codeKey.split("-")[0];
                p.destroy(Long.parseLong(supplierCode));
                businessExecCache.invalidate(newCodeKey);
            }
        }
    }

    @SuppressWarnings("rawtypes")
    public void reloadScriptCache(String code, ERPModule erpModule, String dataSourceId, long supplierCode) {
        if (!isUseScriptService) {
            return;
        }

        String codeKey = supplierCode + "-" + code;
        BusinessExecutor p = businessExecCache.getIfPresent(codeKey);
        if (p != null) {
            p.destroy(supplierCode);
            businessExecCache.invalidate(codeKey);
            loadBusinessExecutor(code, erpModule, dataSourceId, supplierCode);
        } else {
            p = businessExecCache.getIfPresent(0 + "-" + code);
            if (p != null) {
                String newCodeKey = 0 + "-" + code;
                p.destroy(supplierCode);
                businessExecCache.invalidate(newCodeKey);
                loadBusinessExecutor(code, erpModule, dataSourceId, supplierCode);
            }
        }
    }

    /**
     * 获取已加载的业务脚本列表
     *
     * @param dataSourceId 分库
     * @param supplierCode 分表
     * @return 业务脚本列表
     */
    @SuppressWarnings("unused")
    public List<BusinessScript> getLoadedScriptCache(String dataSourceId, long supplierCode) {
        final String codePrefix = supplierCode + "-";

        //2024-01-26 17:00 Init_ExpandoGroovy脚本不提供重载算了，没法用对应的service重载一次，要么对Init_ExpandoGroovy提供单独的方法，每个service都重载一次
        //2024-01-26 18:17 还是多写几行代码，提供下Init_ExpandoGroovy也可以重载，要保持全脚本化，具体查看BE_DocClearScriptCache里面的处理
        return businessExecCache.asMap().entrySet().stream()
                .filter((x) -> x.getKey().startsWith(supplierCode + "-") || x.getKey().startsWith(0 + "-"))
                .map((x) -> {
                    String code = (x.getKey().startsWith("0-")) ? x.getKey().substring("0-".length()) : x.getKey().substring(codePrefix.length());

                    BusinessScript businessScript = docInfoService.getRedisBusinessScript(code, supplierCode);
                    if (businessScript == null) {
                        businessScript = new BusinessScript();
                        businessScript.setBusinessCode(code);
                        businessScript.setBusinessName(x.getValue().scriptName());
                        businessScript.setBusinessModule(x.getValue().module().getValue());
                        businessScript.setCommonFlag(0);
                    }
                    businessScript.setLoadServiceClassName(this.getClass().getSimpleName());
                    return businessScript;
                })
                //.filter((code) -> !StringUtils.equalsIgnoreCase("Init_ExpandoGroovy", code.getBusinessCode()))
                .collect(Collectors.toList());
    }

    //region doId

    /**
     * 操作id
     * 异步针对单id数据操作，如果是同步的话，直接用handleScript即可，参考orderService.voidSendOrder方法
     *
     * @param id              ids字符串，用逗号分隔
     * @param redisKey        redis键值，不要用getRedisKey转换，直接用redisKey定义的常量即可
     * @param actionName      操作名称
     * @param businessCode    脚本编号
     * @param erpModule       脚本模块
     * @param currentUser     操作人
     * @param dataSourceId    分库
     * @param supplierCode    分表
     * @param successConsumer 脚本执行成功之后需要的操作，参数为脚本返回的实际数据<R>
     * @param supplier        脚本参数生成
     * @param executor        执行线程池
     * @param <S>             脚本执行参数类型
     * @param <R>             脚本执行输出类型
     * @return 操作数据
     */
    @SuppressWarnings({"unused", "SameParameterValue"})
    protected <S extends BaseEntity, R extends BaseEntity> RetResult<R> doId(String id,
                                                                             String redisKey,
                                                                             String actionName,
                                                                             String businessCode,
                                                                             ERPModule erpModule,
                                                                             ERPTokenUser currentUser,
                                                                             String dataSourceId,
                                                                             long supplierCode,
                                                                             Function<String, RetResult<R>> checkData,
                                                                             Consumer<R> successConsumer,
                                                                             Supplier<S> supplier,
                                                                             Executor executor) {
        if (StringUtils.isBlank(id)) {
            return RetResult.<R>errorT().retinfo("请传入有效的信息");
        }

        if (lockDataService.hLockAdd(id, redisKey, supplierCode) > 1) {
            return RetResult.<R>errorT().retinfo(actionName + "提交工作中，请稍后刷新即可，无需重复操作");
        }

        //1、数据检查
        if (checkData != null) {
            RetResult<R> checkDataResult = checkData.apply(id);
            if (!checkDataResult.isSuccess()) {
                return checkDataResult;
            }
        }

        try {

            CompletableFuture<RetResult<R>> runFuture = CompletableFuture.supplyAsync(
                    () -> handleScript(businessCode, erpModule, dataSourceId, supplierCode, supplier),
                    executor);

            runFuture.whenComplete((retResult, e) -> {
                lockDataService.hLockDel(id, redisKey, supplierCode);

                if (e == null) {
                    if (retResult.isSuccess()) {
                        if (successConsumer != null) {
                            successConsumer.accept(retResult.getResult());
                        }

                        systemService.sendERPNoticeMessage3(ERPUtils.getSysTokenUser(),
                                String.valueOf(currentUser.getId()),
                                actionName + "完成，可刷新页面处理订单信息", dataSourceId, supplierCode);
                    } else {
                        systemService.sendERPNoticeMessage3(ERPUtils.getSysTokenUser(),
                                String.valueOf(currentUser.getId()),
                                actionName + "失败:" + retResult.getRetinfo(), dataSourceId, supplierCode);
                    }
                } else {
                    systemService.sendERPNoticeMessage3(ERPUtils.getSysTokenUser(),
                            String.valueOf(currentUser.getId()),
                            actionName + "失败:" + e.getMessage(), dataSourceId, supplierCode);
                    logger.error(e.getMessage(), e);
                }
            });
            systemService.sendERPNoticeMessage3(ERPUtils.getSysTokenUser(),
                    String.valueOf(currentUser.getId()), actionName + "进行中，请稍后刷新，不要重复操作", dataSourceId, supplierCode);
            return RetResult.successT();
        } catch (Exception e) {
            lockDataService.hLockDel(id, redisKey, supplierCode);
            logger.error(e.getMessage(), e);
            return RetResult.<R>errorT().retinfo(e.getMessage());
        }
    }

    /**
     * 批量操作id
     * 此方法用于系统中批量操作id，id操作独占，
     *
     * @param ids             ids字符串，用逗号分隔
     * @param redisKey        redis键值，不要用getRedisKey转换，直接用redisKey定义的常量即可
     * @param actionName      操作名称
     * @param businessCode    脚本编号
     * @param erpModule       脚本模块
     * @param currentUser     操作人
     * @param sourceParams    原始参数
     * @param dataSourceId    分库
     * @param supplierCode    分表
     * @param initConsumer    在调用脚本之前需要的初始化操作(例如修改单据状态隐藏，避免查询操作)，参数RMap，为sourceParams内容，加上锁住成功的id和失败的id，成功id键为ERPUtils.PARAM_SUCCESS_IDS
     * @param errorConsumer   在调用脚本失败后需要恢复的操作(例如恢复单据状态)，参数RMap，为sourceParams内容，加上锁住成功的id和失败的id，成功id键为ERPUtils.PARAM_SUCCESS_IDS
     * @param successConsumer 脚本执行成功之后需要的操作，参数为脚本返回的实际数据List<R>,注意，同步情况下，此函数不执行，同步情况下，前端调用接口处理
     * @param supplier        脚本参数生成
     * @param checkSync       检查脚本执行后，是否需要阻塞等待，当前的业务情况基本都是判断操作人的来源，所以参数暂定为currentUser
     * @param executor        执行线程池
     * @param <S>             脚本执行参数类型
     * @param <R>             脚本执行输出类型
     * @return 操作数据
     */
    @SuppressWarnings({"rawtypes", "unchecked", "SameParameterValue"})
    protected <S extends BaseEntity, R extends BaseEntity> RetResult<List<R>> doIds(String ids,
                                                                                    String redisKey,
                                                                                    String actionName,
                                                                                    String businessCode,
                                                                                    ERPModule erpModule,
                                                                                    ERPTokenUser currentUser,
                                                                                    RMap sourceParams,
                                                                                    String dataSourceId,
                                                                                    long supplierCode,
                                                                                    Consumer<RMap> initConsumer,
                                                                                    Consumer<RMap> errorConsumer,
                                                                                    Consumer<List<R>> successConsumer,
                                                                                    SupplierRMap<S> supplier,
                                                                                    Predicate<ERPTokenUser> checkSync,
                                                                                    Executor executor) {
        if (StringUtils.isBlank(ids)) {
            return RetResult.<List<R>>errorT().retinfo("请传入有效的信息");
        }

        //异步操作
        //1、锁定数据
        RedisHLockInfo lockInfo = lockDataService.hMultipleLockAdd(ids, redisKey, supplierCode);
        if (lockInfo == null) {
            return RetResult.<List<R>>errorT().retinfo(actionName + "提交工作中，请稍后刷新即可，无需重复操作");
        }
        if (lockInfo.getError() != 0) {
            return RetResult.<List<R>>errorT().retinfo(lockInfo.getErrorMsg());
        }

        //去掉id中已经被锁定的数据
        List<RedisHLockResult> successList = lockInfo.getNumList().stream().filter((o) -> o.getAfterNum() == 1).toList();
        List<RedisHLockResult> failList = lockInfo.getNumList().stream().filter((o) -> o.getAfterNum() > 1).toList();

        if (!successList.isEmpty()) {
            String successIds = successList.stream().map(RedisHLockResult::getNumField).collect(Collectors.joining(","));
            String failIds = failList.stream().map(RedisHLockResult::getNumField).collect(Collectors.joining(","));
            if (sourceParams == null) {
                sourceParams = new RMap();
            }
            sourceParams.put(ERPUtils.PARAM_SUCCESS_IDS, successIds);
            sourceParams.put(ERPUtils.PARAM_FAIL_IDS, failIds);

            if (initConsumer != null) {
                initConsumer.accept(sourceParams);
            }

            S params = supplier.get(sourceParams);

            //2、执行脚本
            CompletableFuture<RetResult<List<R>>> runFuture = CompletableFuture.supplyAsync(
                    () -> handleScriptList(businessCode, erpModule, dataSourceId, supplierCode, params),
                    executor);

            if (checkSync.test(currentUser)) {
                try {
                    return runFuture.get(20, TimeUnit.SECONDS);
                } catch (InterruptedException | ExecutionException | TimeoutException e) {
                    if (errorConsumer != null) {
                        errorConsumer.accept(sourceParams);
                    }
                    logger.error(e.getMessage(), e);
                    return RetResult.<List<R>>errorT().retcode(HttpCode.INTERNAL_SERVER_ERROR.value()).retinfo(e.getMessage());
                } finally {
                    lockDataService.hMultipleLockDel(ids, redisKey, supplierCode);
                }
            } else {
                RMap finalSourceParams = sourceParams;
                runFuture.whenComplete((retResult, e) -> {
                    lockDataService.hMultipleLockDel(ids, redisKey, supplierCode);
                    if (e == null) {
                        if (retResult.isSuccess()) {

                            if (successConsumer != null) {
                                successConsumer.accept(retResult.getResult());
                            }
                            systemService.sendERPNoticeMessage3(ERPUtils.getSysTokenUser(),
                                    String.valueOf(currentUser.getId()),
                                    actionName + "成功，可刷新页面获取最新状态", dataSourceId, supplierCode);
                        } else {
                            if (errorConsumer != null) {
                                errorConsumer.accept(finalSourceParams);
                            }
                            systemService.sendERPNoticeMessage3(ERPUtils.getSysTokenUser(),
                                    String.valueOf(currentUser.getId()),
                                    actionName + "失败:" + retResult.getRetinfo(), dataSourceId, supplierCode);
                        }
                    } else {
                        if (errorConsumer != null) {
                            errorConsumer.accept(finalSourceParams);
                        }
                        systemService.sendERPNoticeMessage3(ERPUtils.getSysTokenUser(),
                                String.valueOf(currentUser.getId()),
                                actionName + "失败:" + e.getMessage(), dataSourceId, supplierCode);
                        logger.error(e.getMessage(), e);
                    }
                });
                systemService.sendERPNoticeMessage3(ERPUtils.getSysTokenUser(),
                        String.valueOf(currentUser.getId()), actionName + "任务已提交后台，请稍后刷新，不要重复操作", dataSourceId, supplierCode);
                return RetResult.successT();
            }
        } else {
            lockDataService.hMultipleLockDel(ids, redisKey, supplierCode);
            return RetResult.<List<R>>errorT().retinfo("操作进行中，请勿重复提交操作");
        }
    }

    /**
     * 批量操作id
     * 此方法用于系统中批量操作id，id操作独占，
     *
     * @param ids          ids字符串，用逗号分隔
     * @param redisKey     redis键值，不要用getRedisKey转换，直接用redisKey定义的常量即可
     * @param actionName   操作名称
     * @param businessCode 脚本编号
     * @param erpModule    脚本模块
     * @param currentUser  操作人
     * @param sourceParams 原始参数
     * @param dataSourceId 分库
     * @param supplierCode 分表
     * @param supplier     脚本参数生成
     * @param checkSync    检查脚本执行后，是否需要阻塞等待，当前的业务情况基本都是判断操作人的来源，所以参数暂定为currentUser
     * @param executor     执行线程池
     * @param <S>          脚本执行参数类型
     * @param <R>          脚本执行输出类型
     * @return 操作数据
     */
    @SuppressWarnings({"rawtypes", "unused"})
    protected <S extends BaseEntity, R extends BaseEntity> RetResult<List<R>> doIds(String ids,
                                                                                    String redisKey,
                                                                                    String actionName,
                                                                                    String businessCode,
                                                                                    ERPModule erpModule,
                                                                                    ERPTokenUser currentUser,
                                                                                    RMap sourceParams,
                                                                                    String dataSourceId,
                                                                                    long supplierCode,
                                                                                    SupplierRMap<S> supplier,
                                                                                    Predicate<ERPTokenUser> checkSync,
                                                                                    Executor executor) {
        return doIds(ids, redisKey, actionName,
                businessCode, erpModule,
                currentUser,
                sourceParams, dataSourceId, supplierCode,
                null, null, null, supplier,
                checkSync, executor);
    }

    //endregion

    //region ES通用查询

    /**
     * 不分页查询es的通用写法
     *
     * @param esClient    es客户端
     * @param jsonConvert json转换器
     * @param clazz       返回类型
     * @param searchData  查询参数
     * @param qbe         查询条件构造器
     * @param index       查询索引
     * @param sbe         排序构造器
     * @param <T>         对象
     * @return 对象数组
     */
    @SuppressWarnings({"rawtypes", "DuplicatedCode", "SameParameterValue"})
    protected <T> List<T> searchESList(ESClient esClient, JsonConvert jsonConvert,
                                       Class<T> clazz,
                                       RMap searchData, QueryBuilderExecutor qbe,
                                       String index, SortBuilderExecutor... sbe) {
        QueryBuilder qb = qbe.execute(searchData);

        SearchRequestBuilder requestBuilder = esClient.getClient().prepareSearch(index)
                .setQuery(qb);

        if (sbe != null) {
            for (SortBuilderExecutor sortBuilderExecutor : sbe) {
                if (sortBuilderExecutor != null) {
                    requestBuilder = requestBuilder.addSort(sortBuilderExecutor.execute());
                }
            }
        }

        SearchResponse scrollResp = requestBuilder.setSize(500)
                .setScroll(new TimeValue(60000))
                .execute()
                .actionGet();

        List<T> resultList = new ArrayList<>();
        do {
            for (SearchHit hit : scrollResp.getHits().getHits()) {
                String json = hit.getSourceAsString();
                T result = jsonConvert.convertFrom(clazz, json);

                resultList.add(result);
            }

            scrollResp = esClient.getClient().prepareSearchScroll(scrollResp.getScrollId()).setScroll(new TimeValue(60000)).execute().actionGet();
        } while (scrollResp.getHits().getHits().length != 0);

        return resultList;
    }

    /**
     * 不分页查询es的通用写法
     *
     * @param esClient    es客户端
     * @param jsonConvert json转换器
     * @param clazz       返回类型
     * @param searchData  查询参数
     * @param qbe         查询条件构造器
     * @param index       查询索引
     * @param sbe         排序构造器
     * @param <T>         对象
     * @return 对象数组
     */
    @SuppressWarnings({"rawtypes", "DuplicatedCode", "SameParameterValue"})
    protected <T> List<T> searchESList(ESClient esClient, JsonConvert jsonConvert,
                                       Class<T> clazz,
                                       RMap searchData, QueryBuilderExecutor qbe,
                                       ESDataInfoExecutor<T> de,
                                       String index, SortBuilderExecutor... sbe) {
        QueryBuilder qb = qbe.execute(searchData);

        SearchRequestBuilder requestBuilder = esClient.getClient().prepareSearch(index)
                .setQuery(qb);

        if (sbe != null) {
            for (SortBuilderExecutor sortBuilderExecutor : sbe) {
                if (sortBuilderExecutor != null) {
                    requestBuilder = requestBuilder.addSort(sortBuilderExecutor.execute());
                }
            }
        }

        SearchResponse scrollResp = requestBuilder.setSize(500)
                .setScroll(new TimeValue(60000))
                .execute()
                .actionGet();

        List<T> resultList = new ArrayList<>();
        do {
            for (SearchHit hit : scrollResp.getHits().getHits()) {
                String json = hit.getSourceAsString();
                T result = jsonConvert.convertFrom(clazz, json);
                if (de != null) {
                    de.execute(result);
                }
                resultList.add(result);
            }

            scrollResp = esClient.getClient().prepareSearchScroll(scrollResp.getScrollId()).setScroll(new TimeValue(60000)).execute().actionGet();
        } while (scrollResp.getHits().getHits().length != 0);

        return resultList;
    }

    /**
     * 不分页查询es的通用写法
     *
     * @param esClient    es客户端
     * @param jsonConvert json转换器
     * @param clazz       返回类型
     * @param searchData  查询参数
     * @param qbe         查询条件构造器
     * @param index       查询索引
     * @param sbe         排序构造器
     * @param <T>         对象
     * @return 对象数组
     */
    @SuppressWarnings({"rawtypes", "DuplicatedCode", "unused"})
    protected <T> List<T> searchESList(ESClient esClient, JsonConvert jsonConvert,
                                       Class<T> clazz,
                                       RMap searchData, QueryBuilderExecutor qbe,
                                       ESHitInfoExecutor<T> de,
                                       String[] index, SortBuilderExecutor... sbe) {
        QueryBuilder qb = qbe.execute(searchData);

        SearchRequestBuilder requestBuilder = esClient.getClient().prepareSearch(index)
                .setQuery(qb);

        if (sbe != null) {
            for (SortBuilderExecutor sortBuilderExecutor : sbe) {
                if (sortBuilderExecutor != null) {
                    requestBuilder = requestBuilder.addSort(sortBuilderExecutor.execute());
                }
            }
        }

        SearchResponse scrollResp = requestBuilder.setSize(500)
                .setScroll(new TimeValue(60000))
                .execute()
                .actionGet();

        List<T> resultList = new ArrayList<>();
        do {
            for (SearchHit hit : scrollResp.getHits().getHits()) {
                resultList.add(de.execute(hit));
            }

            scrollResp = esClient.getClient().prepareSearchScroll(scrollResp.getScrollId()).setScroll(new TimeValue(60000)).execute().actionGet();
        } while (scrollResp.getHits().getHits().length != 0);

        return resultList;
    }

    /**
     * 不分页查询es的通用写法
     *
     * @param esClient    es客户端
     * @param jsonConvert json转换器
     * @param clazz       返回类型
     * @param searchData  查询参数
     * @param qbe         查询条件构造器
     * @param index       查询索引
     * @param sbe         排序构造器
     * @param <T>         对象
     * @return 对象数组
     */
    @SuppressWarnings({"rawtypes", "DuplicatedCode", "unused"})
    protected <T> List<T> searchESList(ESClient esClient, JsonConvert jsonConvert,
                                       Class<T> clazz,
                                       RMap searchData, QueryBuilderExecutor qbe,
                                       ESDataInfoExecutor<T> de,
                                       String[] index, SortBuilderExecutor... sbe) {
        QueryBuilder qb = qbe.execute(searchData);

        SearchRequestBuilder requestBuilder = esClient.getClient().prepareSearch(index)
                .setQuery(qb);

        if (sbe != null) {
            for (SortBuilderExecutor sortBuilderExecutor : sbe) {
                if (sortBuilderExecutor != null) {
                    requestBuilder = requestBuilder.addSort(sortBuilderExecutor.execute());
                }
            }
        }

        SearchResponse scrollResp = requestBuilder.setSize(500)
                .setScroll(new TimeValue(60000))
                .execute()
                .actionGet();

        List<T> resultList = new ArrayList<>();
        do {
            for (SearchHit hit : scrollResp.getHits().getHits()) {
                String json = hit.getSourceAsString();
                T result = jsonConvert.convertFrom(clazz, json);
                if (de != null) {
                    de.execute(result);
                }
                resultList.add(result);
            }

            scrollResp = esClient.getClient().prepareSearchScroll(scrollResp.getScrollId()).setScroll(new TimeValue(60000)).execute().actionGet();
        } while (scrollResp.getHits().getHits().length != 0);

        return resultList;
    }

    @SuppressWarnings({"rawtypes", "DuplicatedCode", "unused"})
    protected <T> Map<Long, T> searchESMap(ESClient esClient, JsonConvert jsonConvert,
                                           Class<T> clazz,
                                           RMap searchData, QueryBuilderExecutor qbe,
                                           ToLongFunction<T> tl,
                                           String index, SortBuilderExecutor... sbe) {
        QueryBuilder qb = qbe.execute(searchData);

        SearchRequestBuilder requestBuilder = esClient.getClient().prepareSearch(index)
                .setQuery(qb);

        if (sbe != null) {
            for (SortBuilderExecutor sortBuilderExecutor : sbe) {
                if (sortBuilderExecutor != null) {
                    requestBuilder = requestBuilder.addSort(sortBuilderExecutor.execute());
                }
            }
        }

        SearchResponse scrollResp = requestBuilder.setSize(500)
                .setScroll(new TimeValue(60000))
                .execute()
                .actionGet();

        HashMap<Long, T> dataMap = new HashMap<>();
        do {
            for (SearchHit hit : scrollResp.getHits().getHits()) {
                String json = hit.getSourceAsString();
                T result = jsonConvert.convertFrom(clazz, json);
                long iValue = tl.applyAsLong(result);
                if (!dataMap.containsKey(iValue)) {
                    dataMap.put(iValue, result);
                }
            }

            scrollResp = esClient.getClient().prepareSearchScroll(scrollResp.getScrollId()).setScroll(new TimeValue(60000)).execute().actionGet();
        } while (scrollResp.getHits().getHits().length != 0);

        return dataMap;
    }


    @SuppressWarnings({"rawtypes", "DuplicatedCode", "SameParameterValue", "unused"})
    protected <T> Map<Long, List<T>> searchESMapList(long[] src,
                                                     ESClient esClient, JsonConvert jsonConvert,
                                                     Class<T> clazz,
                                                     RMap searchData, QueryBuilderExecutor qbe,
                                                     ToLongFunction<T> tl,
                                                     String index, SortBuilderExecutor... sbe) {
        QueryBuilder qb = qbe.execute(searchData);

        SearchRequestBuilder requestBuilder = esClient.getClient().prepareSearch(index)
                .setQuery(qb);

        if (sbe != null) {
            for (SortBuilderExecutor sortBuilderExecutor : sbe) {
                if (sortBuilderExecutor != null) {
                    requestBuilder = requestBuilder.addSort(sortBuilderExecutor.execute());
                }
            }
        }

        SearchResponse scrollResp = requestBuilder.setSize(500)
                .setScroll(new TimeValue(60000))
                .execute()
                .actionGet();

        HashMap<Long, List<T>> dataMap = new HashMap<>();
        do {
            for (SearchHit hit : scrollResp.getHits().getHits()) {
                String json = hit.getSourceAsString();
                T result = jsonConvert.convertFrom(clazz, json);
                long iValue = tl.applyAsLong(result);
                if (!dataMap.containsKey(iValue)) {
                    dataMap.put(iValue, new ArrayList<>());
                }
                dataMap.get(iValue).add(result);
            }

            scrollResp = esClient.getClient().prepareSearchScroll(scrollResp.getScrollId()).setScroll(new TimeValue(60000)).execute().actionGet();
        } while (scrollResp.getHits().getHits().length != 0);

        Arrays.stream(src).forEach((l) -> {
            if (!dataMap.containsKey(l)) {
                dataMap.put(l, new ArrayList<>());
            }
        });

        return dataMap;
    }

    /**
     * 分页查询es的通用写法
     *
     * @param esClient    es客户端
     * @param jsonConvert json转换器
     * @param clazz       返回类型
     * @param searchData  查询参数
     * @param qbe         查询条件构造器
     * @param index       查询索引
     * @param sbe         排序构造器
     * @param pageFlipper 分页参数
     * @param <T>         对象
     * @return 对象数组
     */
    @SuppressWarnings({"rawtypes", "DuplicatedCode", "unused", "SameParameterValue"})
    protected <T> List<T> searchESList(ESClient esClient, JsonConvert jsonConvert,
                                       Class<T> clazz,
                                       RMap searchData, QueryBuilderExecutor qbe,
                                       String index, PageFlipper pageFlipper,
                                       SortBuilderExecutor... sbe) {
        QueryBuilder qb = qbe.execute(searchData);

        SearchRequestBuilder requestBuilder = esClient.getClient().prepareSearch(index)
                .setQuery(qb);

        if (sbe != null) {
            for (SortBuilderExecutor sortBuilderExecutor : sbe) {
                if (sortBuilderExecutor != null) {
                    requestBuilder = requestBuilder.addSort(sortBuilderExecutor.execute());
                }
            }
        }

        requestBuilder = requestBuilder.setSize(pageFlipper.getPageSize())
                .setFrom(pageFlipper.getPageSize() * (pageFlipper.getPageIndex() - 1));


        SearchHits searchHits = requestBuilder.execute().actionGet().getHits();

        List<T> resultList = new ArrayList<>();
        searchHits.forEach(x -> {
            String json = x.getSourceAsString();
            T result = jsonConvert.convertFrom(clazz, json);

            resultList.add(result);
        });

        pageFlipper.setTotal(searchHits.getTotalHits());

        return resultList;
    }

    @SuppressWarnings({"rawtypes", "DuplicatedCode", "unused", "SameParameterValue"})
    protected <T> List<T> searchESList(ESClient esClient, JsonConvert jsonConvert,
                                       Class<T> clazz,
                                       RMap searchData, QueryBuilderExecutor qbe,
                                       ESDataInfoExecutor<T> de,
                                       String index, PageFlipper pageFlipper,
                                       SortBuilderExecutor... sbe) {
        if (pageFlipper == null) {
            //加这个判断的目的是为了调用方统一代码，避免写了半天，调用方还要判断是否需要分页获取
            return searchESList(esClient, jsonConvert, clazz, searchData, qbe, de, index, sbe);
        }

        QueryBuilder qb = qbe.execute(searchData);

        SearchRequestBuilder requestBuilder = esClient.getClient().prepareSearch(index)
                .setQuery(qb);

        if (sbe != null) {
            for (SortBuilderExecutor sortBuilderExecutor : sbe) {
                if (sortBuilderExecutor != null) {
                    requestBuilder = requestBuilder.addSort(sortBuilderExecutor.execute());
                }
            }
        }

        requestBuilder = requestBuilder.setSize(pageFlipper.getPageSize())
                .setFrom(pageFlipper.getPageSize() * (pageFlipper.getPageIndex() - 1));


        SearchHits searchHits = requestBuilder.execute().actionGet().getHits();

        List<T> resultList = new ArrayList<>();
        searchHits.forEach(x -> {
            String json = x.getSourceAsString();
            T result = jsonConvert.convertFrom(clazz, json);
            if (de != null) {
                de.execute(result);
            }
            resultList.add(result);
        });

        pageFlipper.setTotal(searchHits.getTotalHits());

        return resultList;
    }

    /**
     * 通过id在es中获取单个记录信息的方法
     *
     * @param esClient    es客户端
     * @param jsonConvert json转换器
     * @param clazz       返回类型
     * @param id          查询id
     * @param index       查询索引
     * @param type        查询type
     * @param <T>         对象
     * @return 对象数组
     */
    @Local
    @SuppressWarnings("DuplicatedCode")
    public <T> T getESOneInfo(ESClient esClient, JsonConvert jsonConvert,
                              Class<T> clazz,
                              String id,
                              String index, String type,
                              ESHitInfoExecutor<T> de) {
        BoolQueryBuilder qb = QueryBuilders.boolQuery()
                .must(QueryBuilders.typeQuery(type))
                .must(QueryBuilders.idsQuery(type).addIds(id));

        SearchResponse scrollResp = esClient.getClient().prepareSearch(index)
                .setTypes(type)
                .setQuery(qb)
                .setSize(1)
                .execute()
                .actionGet();
        if (scrollResp.getHits().getTotalHits() == 0) {
            return null;
        } else {
            SearchHit hit = scrollResp.getHits().getHits()[0];
            if (de != null) {
                return de.execute(hit);
            } else {
                return jsonConvert.convertFrom(clazz, hit.getSourceAsString());
            }
        }
    }

    /**
     * 通过id在es中获取单个记录信息的方法
     *
     * @param esClient    es客户端
     * @param jsonConvert json转换器
     * @param clazz       返回类型
     * @param index       查询索引
     * @param <T>         对象
     * @return 对象数组
     */
    @Local
    @SuppressWarnings({"DuplicatedCode", "unused"})
    public <T> T getESOneInfo(ESClient esClient, JsonConvert jsonConvert,
                              Class<T> clazz,
                              QueryBuilderExecutor qbe,
                              String index,
                              ESHitInfoExecutor<T> de) {
        QueryBuilder qb = qbe.execute(null);


        SearchResponse scrollResp = esClient.getClient().prepareSearch(index)
                .setQuery(qb)
                .setSize(1)
                .execute()
                .actionGet();
        if (scrollResp.getHits().getTotalHits() == 0) {
            return null;
        } else {
            SearchHit hit = scrollResp.getHits().getHits()[0];
            if (de != null) {
                return de.execute(hit);
            } else {
                return jsonConvert.convertFrom(clazz, hit.getSourceAsString());
            }
        }
    }

    /**
     * 通过id在es中获取单个记录信息的方法
     *
     * @param esClient    es客户端
     * @param jsonConvert json转换器
     * @param clazz       返回类型
     * @param indexs      查询索引
     * @param <T>         对象
     * @return 对象数组
     */
    @Local
    @SuppressWarnings({"DuplicatedCode", "unused"})
    public <T> T getESOneInfo(ESClient esClient, JsonConvert jsonConvert,
                              Class<T> clazz,
                              QueryBuilderExecutor qbe,
                              String[] indexs,
                              ESHitInfoExecutor<T> de) {
        QueryBuilder qb = qbe.execute(null);


        SearchResponse scrollResp = esClient.getClient().prepareSearch(indexs)
                .setQuery(qb)
                .setSize(1)
                .execute()
                .actionGet();
        if (scrollResp.getHits().getTotalHits() == 0) {
            return null;
        } else {
            SearchHit hit = scrollResp.getHits().getHits()[0];
            if (de != null) {
                return de.execute(hit);
            } else {
                return jsonConvert.convertFrom(clazz, hit.getSourceAsString());
            }
        }
    }

    @SuppressWarnings({"DuplicatedCode", "unused", "RedundantSuppression", "SameParameterValue"})
    protected <T> T getESOneInfo(ESClient esClient, JsonConvert jsonConvert,
                                 Class<T> clazz,
                                 QueryBuilderExecutor qbe,
                                 String index,
                                 ESHitInfoExecutor<T> de,
                                 SortBuilderExecutor... sbe) {
        QueryBuilder qb = qbe.execute(null);
        SearchRequestBuilder requestBuilder = esClient.getClient().prepareSearch(index)
                .setQuery(qb);

        if (sbe != null) {
            for (SortBuilderExecutor sortBuilderExecutor : sbe) {
                if (sortBuilderExecutor != null) {
                    requestBuilder = requestBuilder.addSort(sortBuilderExecutor.execute());
                }
            }
        }

        requestBuilder = requestBuilder.setSize(1);


        SearchHits searchHits = requestBuilder.execute().actionGet().getHits();
        if (searchHits.getTotalHits() == 0) {
            return null;
        } else {
            SearchHit hit = searchHits.getHits()[0];
            if (de != null) {
                return de.execute(hit);
            } else {
                return jsonConvert.convertFrom(clazz, hit.getSourceAsString());
            }
        }
    }

    //endregion

}
