Przeglądaj źródła

差旅标准:出差地域、交通工具标准、住宿补助标准推送胜意接口开发

dingsixi 3 tygodni temu
rodzic
commit
6a27973cb0

+ 226 - 0
base/nckd-base-common/src/main/java/nckd/base/common/utils/HttpUtils.java

@@ -0,0 +1,226 @@
+package nckd.base.common.utils;
+
+import com.alibaba.fastjson.JSONObject;
+
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+/**
+ * @description:http请求完整工具类
+ * @author: dingsixi
+ * @create: 2025/12/16 15:08
+ */
+public class HttpUtils {
+
+    /**
+     * 发送POST请求(JSON格式)
+     */
+    public static String postJson(String url, Map<String, Object> params) throws IOException {
+        return post(url, params, "application/json");
+    }
+
+    /**
+     * 发送POST请求(表单格式)
+     */
+    public static String postForm(String url, Map<String, Object> params) throws IOException {
+        return post(url, params, "application/x-www-form-urlencoded");
+    }
+
+    /**
+     * 发送POST请求
+     */
+    private static String post(String url, Map<String, Object> params, String contentType)
+            throws IOException {
+
+        HttpURLConnection conn = null;
+        StringBuilder response = new StringBuilder();
+
+        try {
+            conn = (HttpURLConnection) new URL(url).openConnection();
+            configConnection(conn, "POST", contentType);
+
+            // 发送请求体
+            String requestBody;
+            if ("application/json".equals(contentType)) {
+                requestBody = JSONObject.toJSONString(params);
+            } else {
+                requestBody = mapToFormUrlEncoded(params);
+            }
+
+            sendRequestBody(conn, requestBody);
+
+            // 获取响应
+            int statusCode = conn.getResponseCode();
+
+            try (BufferedReader reader = new BufferedReader(
+                    new InputStreamReader(
+                            statusCode >= 200 && statusCode < 300 ?
+                                    conn.getInputStream() : conn.getErrorStream(),
+                            StandardCharsets.UTF_8))) {
+
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    response.append(line);
+                }
+            }
+
+            if (statusCode >= 300) {
+                throw new IOException("HTTP Error " + statusCode + ": " + response);
+            }
+
+        } finally {
+            if (conn != null) conn.disconnect();
+        }
+
+        return response.toString();
+    }
+
+    /**
+     * 发送GET请求
+     */
+    public static String get(String url, Map<String, Object> params) throws IOException {
+        String queryString = mapToQueryString(params);
+        String fullUrl = queryString.isEmpty() ? url : url + "?" + queryString;
+
+        HttpURLConnection conn = null;
+        StringBuilder response = new StringBuilder();
+
+        try {
+            conn = (HttpURLConnection) new URL(fullUrl).openConnection();
+            configConnection(conn, "GET", null);
+
+            int statusCode = conn.getResponseCode();
+
+            try (BufferedReader reader = new BufferedReader(
+                    new InputStreamReader(
+                            statusCode == 200 ? conn.getInputStream() : conn.getErrorStream(),
+                            StandardCharsets.UTF_8))) {
+
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    response.append(line);
+                }
+            }
+
+        } finally {
+            if (conn != null) conn.disconnect();
+        }
+
+        return response.toString();
+    }
+
+    /**
+     * 配置连接
+     */
+    private static void configConnection(HttpURLConnection conn, String method,
+                                         String contentType) throws IOException {
+        conn.setRequestMethod(method);
+        conn.setConnectTimeout(15000);
+        conn.setReadTimeout(30000);
+
+        if (contentType != null) {
+            conn.setRequestProperty("Content-Type", contentType);
+        }
+        conn.setRequestProperty("Accept", "*/*");
+        conn.setRequestProperty("User-Agent", "Java HttpClient");
+
+        if ("POST".equals(method)) {
+            conn.setDoOutput(true);
+        }
+    }
+
+    /**
+     * Map转JSON(简易实现)
+     */
+    private static String mapToJson(Map<String, Object> params) {
+        if (params == null || params.isEmpty()) {
+            return "{}";
+        }
+
+        StringBuilder json = new StringBuilder("{");
+        boolean first = true;
+
+        for (Map.Entry<String, Object> entry : params.entrySet()) {
+            if (!first) {
+                json.append(",");
+            }
+
+            json.append("\"")
+                    .append(entry.getKey())
+                    .append("\":");
+
+            Object value = entry.getValue();
+            if (value == null) {
+                json.append("null");
+            } else if (value instanceof String) {
+                json.append("\"")
+                        .append(value.toString().replace("\"", "\\\""))
+                        .append("\"");
+            } else {
+                json.append(value);
+            }
+
+            first = false;
+        }
+
+        json.append("}");
+        return json.toString();
+    }
+
+    /**
+     * Map转表单编码
+     */
+    private static String mapToFormUrlEncoded(Map<String, Object> params)
+            throws UnsupportedEncodingException {
+
+        if (params == null || params.isEmpty()) {
+            return "";
+        }
+
+        StringBuilder result = new StringBuilder();
+        boolean first = true;
+
+        for (Map.Entry<String, Object> entry : params.entrySet()) {
+            if (!first) {
+                result.append("&");
+            }
+
+            result.append(URLEncoder.encode(entry.getKey(), "UTF-8"))
+                    .append("=")
+                    .append(URLEncoder.encode(
+                            entry.getValue() == null ? "" : entry.getValue().toString(),
+                            "UTF-8"));
+
+            first = false;
+        }
+
+        return result.toString();
+    }
+
+    /**
+     * Map转查询字符串
+     */
+    private static String mapToQueryString(Map<String, Object> params)
+            throws UnsupportedEncodingException {
+        return mapToFormUrlEncoded(params);
+    }
+
+    /**
+     * 发送请求体
+     */
+    private static void sendRequestBody(HttpURLConnection conn, String body)
+            throws IOException {
+
+        if (body != null && !body.isEmpty()) {
+            try (OutputStream os = conn.getOutputStream();
+                 OutputStreamWriter writer = new OutputStreamWriter(os, StandardCharsets.UTF_8)) {
+                writer.write(body);
+                writer.flush();
+            }
+        }
+    }
+}

+ 56 - 0
base/nckd-base-common/src/main/java/nckd/base/common/utils/ParamUtils.java

@@ -0,0 +1,56 @@
+package nckd.base.common.utils;
+
+import kd.bos.entity.AppInfo;
+import kd.bos.entity.AppMetadataCache;
+import kd.bos.entity.param.AppParam;
+import kd.bos.servicehelper.org.OrgUnitServiceHelper;
+import kd.bos.servicehelper.parameter.SystemParamServiceHelper;
+
+import java.util.Map;
+
+/**
+ * @description:系统参数工具类
+ * @author: dingsixi
+ * @create: 2025/12/16 15:08
+ */
+public class ParamUtils {
+
+    /**
+     * 费用核算
+     */
+    public static final String EM = "em";
+
+
+    /**
+     *     私有构造方法(防御反射实例化)
+     */
+    private ParamUtils() {
+        throw new AssertionError("Utility class cannot be instantiated");
+    }
+
+    /**
+     *
+     * @param appNumber 应用编码
+     * @param paramPro 参数标识
+     * @return 应用参数值
+     */
+    public static Object getSysCtrlParameter(String appNumber, String paramPro) {
+        Map<String, Object> sysParaMap = getSysCtrlParameter(appNumber);
+        return sysParaMap == null ? null : sysParaMap.get(paramPro);
+    }
+
+    /**
+     *
+     * @param appNumber 应用参数
+     * @return 所有应用参数值
+     */
+    public static Map<String, Object> getSysCtrlParameter(String appNumber) {
+        AppParam appParam = new AppParam();
+        AppInfo appInfo = AppMetadataCache.getAppInfo(appNumber);
+        String appId = appInfo.getId();
+        appParam.setAppId(appId);
+        appParam.setOrgId(OrgUnitServiceHelper.getRootOrgId());
+        return SystemParamServiceHelper.loadAppParameterFromCache(appParam);
+    }
+
+}

+ 310 - 0
base/nckd-base-common/src/main/java/nckd/base/common/utils/TripSyncBillUtils.java

@@ -0,0 +1,310 @@
+package nckd.base.common.utils;
+
+import com.alibaba.fastjson.JSONObject;
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.exception.KDBizException;
+import kd.bos.logging.Log;
+import kd.bos.logging.LogFactory;
+import nckd.base.common.constant.BaseFieldConst;
+import org.apache.commons.lang3.ObjectUtils;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * @description:商旅同步单据工具类
+ * @author: dingsixi
+ * @create: 2025/12/16 15:22
+ */
+public class TripSyncBillUtils {
+    private final static Log logger = LogFactory.getLog(TripSyncBillUtils.class);
+
+    /**
+     * 火车座位等级,分别是商务座 特等座 一等座 二等座,胜意只有这四种
+     */
+    private final static Map<String, String> trainMap = new HashMap<String, String>() {{
+        put("01", "c-9");
+        put("00", "P");
+        put("02", "M");
+        put("03", "O");
+    }};
+    /**
+     * 飞机座位等级,分别是1头等舱2公务舱3经济舱4高端经济舱,胜意只有这四种
+     */
+    private final static Map<String, String> airMap = new HashMap<String, String>() {{
+        put("01", "1");
+        put("02", "2");
+        put("03", "3");
+        put("04", "4");
+    }};
+
+    /**
+     * @param accommodation 住宿补助标准
+     * @return 将住宿补助标准单据数据转换成接口的业务数据
+     */
+    public static Map<String, Object> getAccommodationDataParam(DynamicObject accommodation) {
+        String displayName = accommodation.getDataEntityType().getDisplayName().getLocaleValue();
+        String billNumber = accommodation.getString(BaseFieldConst.NUMBER);
+
+        logger.info(String.format("推送%s,单据编号%s开始转换业务数据", displayName, billNumber));
+        // 接口业务数据
+        Map<String, Object> data = new HashMap<>();
+        //法人公司代码(为空时 默认为集团下差标数据)
+        data.put("companyCode", "");
+        //差标主表ID,传了是修改 不传是新增
+        data.put("travelStandardId", accommodation.getString(BaseFieldConst.ID));
+        //差标名称(同一维度下要求唯一)
+        data.put("travelStandardsName", accommodation.getString(BaseFieldConst.NAME));
+        //是否时报实销 1是0否 默认为0
+        data.put("isRealTimeReimbursement", "0");
+        //差标生效日期(yyyy-MM-dd)
+        data.put("effectiveDate", DateUtil.date2str(new Date(), DateUtil.DATE_FORMAT_YYYY_MM_DD));
+
+        //标准明细
+        DynamicObjectCollection entryEntity = accommodation.getDynamicObjectCollection(BaseFieldConst.ENTRY_ENTITY);
+        List<Map<String, Object>> dHotelTravelStandardsList = new ArrayList<>();
+        for (DynamicObject entry : entryEntity) {
+            Map<String, Object> dHotelTravelStandards = new HashMap<>();
+            //币种
+            DynamicObject currency = entry.getDynamicObject("currency");
+            String currencyNumber = currency.getString(BaseFieldConst.NUMBER);
+            String currencyName = currency.getString(BaseFieldConst.NAME);
+            //出差地域
+            DynamicObject tripArea = entry.getDynamicObject("triparea");
+            String tripAreaNumber = tripArea.getString(BaseFieldConst.NUMBER);
+            String tripAreaName = tripArea.getString(BaseFieldConst.NAME);
+            //出差地域.旺季区间
+            DynamicObjectCollection dateEntry = tripArea.getDynamicObjectCollection("dateentry");
+            //标准(人天)
+            BigDecimal standardAmount = entry.getBigDecimal("standardamount");
+            //旺季标准(人天)
+            BigDecimal highSeasonStandardAmount = entry.getBigDecimal("highseasonstandardamount");
+
+            //管控方式 1只做提醒 2不允许预订 3不显示 4能订但只能在线支付
+            dHotelTravelStandards.put("controlType", "4");
+            //币种编号(默认人民币CNY)
+            dHotelTravelStandards.put("currencyCode", currencyNumber);
+            //币种名称
+            dHotelTravelStandards.put("currencyName", currencyName);
+            //城市分类编号
+            dHotelTravelStandards.put("cityClassifyNumber", tripAreaNumber);
+            //城市分类名称
+            dHotelTravelStandards.put("cityClassifyName", tripAreaName);
+            //淡季差标金额
+            dHotelTravelStandards.put("lowSeasonPrice", ObjectUtils.isEmpty(standardAmount) ? BigDecimal.ZERO : standardAmount);
+            //旺季差标金额
+            dHotelTravelStandards.put("busySeasonPrice", ObjectUtils.isEmpty(highSeasonStandardAmount) ? BigDecimal.ZERO : highSeasonStandardAmount);
+            //旺季月份 多个月份逗号隔开(例:1,2,3,4,5,6,7,8)
+            dHotelTravelStandards.put("busySeasonMonths", processMonthRanges(dateEntry));
+
+            dHotelTravelStandardsList.add(dHotelTravelStandards);
+        }
+        //国内酒店差标信息
+        data.put("dHotelTravelStandardsList", dHotelTravelStandardsList);
+
+        //报销级别
+        DynamicObjectCollection reimburseLevelCol = accommodation.getDynamicObjectCollection("reimburselevel");
+        //获取适用范围信息
+        List<Map<String, Object>> travelStandardScopesList = getTravelStandardScopesList(reimburseLevelCol);
+        data.put("travelStandardScopesList", travelStandardScopesList);
+
+        logger.info(String.format("推送%s,单据编号%s转换业务数据结束,业务数据JSON:\n%s",
+                displayName, billNumber, JSONObject.toJSONString(data)));
+        return data;
+    }
+
+    /**
+     * @param vehicleStd 交通工具标准
+     * @return 将交通工具标准单据数据转换成接口的业务数据
+     */
+    public static Map<String, Object> getVehicleStdDataParam(DynamicObject vehicleStd) {
+        String displayName = vehicleStd.getDataEntityType().getDisplayName().getLocaleValue();
+        String billNumber = vehicleStd.getString(BaseFieldConst.NUMBER);
+
+        logger.info(String.format("推送%s,单据编号%s开始转换业务数据", displayName, billNumber));
+        //标准类型
+        String stdType = vehicleStd.getString("standardtype");
+        //报销级别
+        DynamicObjectCollection reimburseLevelCol = vehicleStd.getDynamicObjectCollection("reimburselevel");
+
+        // 接口业务数据
+        Map<String, Object> data = new HashMap<>();
+        //法人公司代码(为空时 默认为集团下差标数据)
+        data.put("companyCode", "");
+        //差标主表ID,传了是修改 不传是新增
+        data.put("travelStandardId", vehicleStd.getString(BaseFieldConst.ID));
+        //差标名称(同一维度下要求唯一)
+        data.put("travelStandardsName", vehicleStd.getString(BaseFieldConst.NAME));
+        //是否时报实销 1是0否 默认为0
+        data.put("isRealTimeReimbursement", "0");
+        //差标生效日期(yyyy-MM-dd)
+        data.put("effectiveDate", DateUtil.date2str(new Date(), DateUtil.DATE_FORMAT_YYYY_MM_DD));
+
+        //座位等级
+        DynamicObjectCollection entryEntity = vehicleStd.getDynamicObjectCollection("entry");
+        if (stdType.equals("train")) {
+            List<Map<String, Object>> trainTravelStandardList = getTrainTravelStandardList(entryEntity);
+            data.put("trainTravelStandardList", trainTravelStandardList);
+        } else {
+            //获取国内机票差标信息
+            List<Map<String, Object>> dTicketTravelStandardList = getDTicketTravelStandardList(entryEntity);
+            data.put("dTicketTravelStandardList", dTicketTravelStandardList);
+        }
+
+
+        //获取适用范围信息
+        List<Map<String, Object>> travelStandardScopesList = getTravelStandardScopesList(reimburseLevelCol);
+        data.put("travelStandardScopesList", travelStandardScopesList);
+
+        logger.info(String.format("推送%s,单据编号%s转换业务数据结束,业务数据JSON:\n%s",
+                displayName, billNumber, JSONObject.toJSONString(data)));
+        return data;
+    }
+
+    /**
+     * @param tripArea 出差地域
+     * @return 将出差地域单据数据转换成接口的业务数据
+     */
+    public static Map<String, Object> getTripAreaBillDataParam(DynamicObject tripArea) {
+        String displayName = tripArea.getDataEntityType().getDisplayName().getLocaleValue();
+        String billNumber = tripArea.getString(BaseFieldConst.NUMBER);
+
+        logger.info(String.format("推送%s,单据编号%s开始转换业务数据", displayName, billNumber));
+
+        // 城市分类信息集合
+        List<Map<String, Object>> cityClassifyInfoList = new ArrayList<>();
+
+        // 构建城市列表
+        List<Map<String, Object>> cityList = tripArea.getDynamicObjectCollection(BaseFieldConst.ENTRY_ENTITY)
+                .stream()
+                .map(entry -> {
+                    Map<String, Object> cityInfo = new HashMap<>();
+                    cityInfo.put("cityNumber", entry.getString("city.number"));
+                    return cityInfo;
+                })
+                .collect(Collectors.toList());
+
+        // 构建城市分类信息
+        Map<String, Object> cityClassifyInfo = new HashMap<>();
+        cityClassifyInfo.put("companyCode", "");
+        cityClassifyInfo.put("cityClassifyName", tripArea.getString(BaseFieldConst.NAME));
+        cityClassifyInfo.put("cityClassifyNumber", billNumber);
+        cityClassifyInfo.put("cityInfo", cityList);
+
+        cityClassifyInfoList.add(cityClassifyInfo);
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("cityClassifyInfo", cityClassifyInfoList);
+
+        logger.info(String.format("推送%s,单据编号%s转换业务数据结束,业务数据JSON:\n%s",
+                displayName, billNumber, JSONObject.toJSONString(data)));
+
+        return data;
+    }
+
+    /**
+     * @param entryEntity 座位等级明细
+     * @return 获取火车票差标信息
+     */
+    private static List<Map<String, Object>> getTrainTravelStandardList(DynamicObjectCollection entryEntity) {
+        //火车票差标信息
+        Map<String, Object> trainTravelStandard = new HashMap<>();
+        //座位等级编码
+        String trainCabinCodes = entryEntity.stream()
+                .filter(it -> trainMap.containsKey(it.getString("seat.number")))
+                .map(it -> trainMap.get(it.getString("seat.number")))
+                .collect(Collectors.joining(","));
+        //座位等级名称
+        String trainCabinName = entryEntity.stream()
+                .filter(it -> trainMap.containsKey(it.getString("seat.name")))
+                .map(it -> trainMap.get(it.getString("seat.name")))
+                .collect(Collectors.joining(","));
+        //管控方式 1只做提醒 2不允许预订 3不显示 4能订但只能在线支付
+        trainTravelStandard.put("controlType", "4");
+        //车次等级 多个逗号隔开 例:c-9,P,M,O
+        trainTravelStandard.put("trainCabinCodes", trainCabinCodes);
+        //火车票车型坐席描述 例:城际:商务座 特等座 一等座 二等座
+        trainTravelStandard.put("trainCabinName", trainCabinName);
+        return Collections.singletonList(trainTravelStandard);
+    }
+
+    /**
+     * @param entryEntity 座位等级明细
+     * @return 获取国内机票差标信息
+     */
+    private static List<Map<String, Object>> getDTicketTravelStandardList(DynamicObjectCollection entryEntity) {
+
+        Map<String, Object> dTicketTravelStandard = new HashMap<>();
+        //座位等级编码
+        String cabinCodes = entryEntity.stream()
+                .filter(it -> airMap.containsKey(it.getString("seat.number")))
+                .map(it -> airMap.get(it.getString("seat.number")))
+                .collect(Collectors.joining(","));
+        List<Map<String, String>> allowedAreaList = new ArrayList<>();
+        Map<String, String> allowedArea = new HashMap<>();
+        //舱位编号1头等舱2公务舱3经济舱4高端经济舱
+        allowedArea.put("cabinCodes", cabinCodes);
+        allowedAreaList.add(allowedArea);
+
+        //管控应用场景(经济舱折扣限制3120803,允许乘坐舱位 3120809,指定舱位折扣信息 3120819)
+        dTicketTravelStandard.put("sceneCode", "3120809");
+        //管控方式 1只做提醒 2不允许预订 3不显示 4能订但只能在线支付
+        dTicketTravelStandard.put("controlType", "4");
+        //指定舱位折扣信息
+        dTicketTravelStandard.put("allowedAreaList", allowedAreaList);
+
+        return Collections.singletonList(dTicketTravelStandard);
+    }
+
+    /**
+     * @param reimburseLevelCol 报销级别
+     * @return 获取适用范围信息
+     */
+    private static List<Map<String, Object>> getTravelStandardScopesList(DynamicObjectCollection reimburseLevelCol) {
+        List<Map<String, Object>> travelStandardScopesList = new ArrayList<>();
+        for (DynamicObject reimburseLevel : reimburseLevelCol) {
+            Map<String, Object> travelStandardScopes = new HashMap<>();
+            //1适用职级、2适用部门、4适用员工、5、适用岗位
+            travelStandardScopes.put("scopesType", "1");
+            //适用范围 存对应的枚举的适用职级编号、适用部门编号、适用员工工号、适用岗位编号
+            travelStandardScopes.put("scopesContent", reimburseLevel.getDynamicObject(1).getString(BaseFieldConst.NUMBER));
+            //适用范围名称 TODO 确认下固定名称
+            travelStandardScopes.put("scopesContentName", "职级范围");
+            travelStandardScopesList.add(travelStandardScopes);
+        }
+        return travelStandardScopesList;
+    }
+
+    /**
+     * @param dateEntry 差旅标准.旺季区间
+     * @return 找出旺季月份
+     */
+    private static String processMonthRanges(DynamicObjectCollection dateEntry) {
+        if (dateEntry.isEmpty()) {
+            return "";
+        }
+
+        return dateEntry.stream()
+                .flatMap(map -> {
+                    int start = Math.min(map.getInt("startmonth"), map.getInt("endmonth"));
+                    int end = Math.max(map.getInt("startmonth"), map.getInt("endmonth"));
+
+                    // 限制月份在有效范围内
+                    start = Math.max(1, Math.min(start, 12));
+                    end = Math.max(1, Math.min(end, 12));
+
+                    // 生成月份范围
+                    return IntStream.rangeClosed(start, end).boxed();
+                })
+                .filter(month -> month >= 1 && month <= 12)
+                .distinct()
+                .sorted()
+                .map(Object::toString)
+                .collect(Collectors.joining(","));
+    }
+
+}

+ 287 - 0
base/nckd-base-common/src/main/java/nckd/base/common/utils/TripSyncUtils.java

@@ -0,0 +1,287 @@
+package nckd.base.common.utils;
+
+import com.alibaba.fastjson.JSONObject;
+import kd.bos.exception.KDBizException;
+import kd.bos.logging.Log;
+import kd.bos.logging.LogFactory;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+
+/**
+ * @description:商旅同步工具类
+ * @author: dingsixi
+ * @create: 2025/12/15 12:00
+ */
+public class TripSyncUtils {
+
+    private final static Log logger = LogFactory.getLog(TripSyncUtils.class);
+
+    /**
+     * 胜意接口请求路径
+     */
+    private final static String PATH = "/fcopen/fcoutapi";
+
+    /**
+     * SIT.胜意接口.KEY
+     */
+    public final static String SIT_FWS_KEY = "011f42937f4049209b5c9fac611020a3";
+    /**
+     * SIT.胜意接口.账号
+     */
+    public final static String ACCOUNT = "JXWL";
+
+    /**
+     * @param service              接口名称
+     * @param urlIsContainsService url是否包含接口名称
+     * @param data                 具体业务数据
+     */
+    public static void pushApi(String service, Boolean urlIsContainsService, Map<String, Object> data) {
+        logger.info(String.format("推送API接口名称:%s,业务数据参数:\n%s", service, JSONObject.toJSONString(data)));
+
+        //获取费用核算应用参数.胜意环境地址
+        String address = (String) ParamUtils.getSysCtrlParameter(ParamUtils.EM, "nckd_address");
+        //公共接口参数
+        Map<String, Object> syCommonParam = TripSyncUtils.createSYCommonParam();
+        syCommonParam.put("service", service);
+        //接口请求路径
+        String url = urlIsContainsService ? address + PATH + "/" + service : address + PATH;
+        //具体业务数据
+        syCommonParam.put("data", data);
+        logger.info(String.format("推送API接口名称:%s,开始推送,接口入参:\n%s", service, JSONObject.toJSONString(syCommonParam)));
+        //接口推送
+        try {
+            String s = HttpUtils.postJson(url, syCommonParam);
+            logger.info(String.format("推送API接口名称:%s,完成推送,接口出参:\n%s", service, s));
+
+            JSONObject result = JSONObject.parseObject(s);
+            if (result.getBoolean("fail")) {
+                throw new KDBizException("推送接口失败:" + result.getString("message"));
+            }
+        } catch (Exception e) {
+            throw new KDBizException(e.getMessage());
+        }
+    }
+
+
+    /**
+     *
+     * @return 生成胜意接口公共请求参数
+     */
+    public static Map<String, Object> createSYCommonParam() {
+        //获取费用核算应用参数
+        Map<String, Object> sysCtrlParameter = ParamUtils.getSysCtrlParameter(ParamUtils.EM);
+        //账号
+        String account = (String) sysCtrlParameter.get("nckd_account");
+        String key = (String) sysCtrlParameter.get("nckd_key");
+        //总公司
+        String company = (String) sysCtrlParameter.get("nckd_company");
+        String timestamp = DateUtil.date2str(new Date(), DateUtil.DATE_TIME_FORMAT_YYYY_MM_DD_HH_MI_SS);
+
+        Map<String, Object> commonParam = new HashMap<>();
+        //格式为yyyy-MM-dd HH:mm:ss,API服务端允许客户端请求时间误差为10分钟。
+        commonParam.put("timestamp", timestamp);
+        //总公司编号由平台提供
+        commonParam.put("compid", company);
+        //账号由平台提供
+        commonParam.put("account", account);
+        commonParam.put("key", key);
+        //返回数据类型默认为1 1表示xml 2 表示json
+        commonParam.put("responseType", "2");
+        //默认为MD5支持MD5签名和国密SM3
+        commonParam.put("signType", "MD5");
+        //签名 md5(account+timestamp+key) Sm3(account+timestamp+key) key由平台提供
+        commonParam.put("sign", createSYSign(timestamp, account, key));
+        //会员ID(非必传,由胜意提供)
+        commonParam.put("hyid", "");
+        //本批次数据的唯一标识
+        commonParam.put("uuid", UUID.randomUUID());
+        return commonParam;
+    }
+
+    /**
+     * @return 生成胜意接口签名
+     */
+    private static String createSYSign(String dateStr, String account, String key) {
+        //账号+当前时间+KEY
+        return getMD5Sign(account + dateStr + key).toLowerCase(Locale.ROOT);
+    }
+
+
+    public static void main(String[] args) {
+        String sign = createTripSyncSign();
+//        String sign = createSYSign();
+        System.out.println(sign);
+    }
+
+
+    /**
+     * @return 生成胜意接口签名,通过main方法生成签名postman调试
+     */
+    private static String createSYSign() {
+        String dateStr = DateUtil.date2str(new Date(), DateUtil.DATE_TIME_FORMAT_YYYY_MM_DD_HH_MI_SS);
+        //账号+当前时间+KEY
+        return String.format("timestamp:%s\nsign:%s", dateStr, getMD5Sign(ACCOUNT + dateStr + SIT_FWS_KEY).toLowerCase(Locale.ROOT));
+    }
+
+    /**
+     * @return 生成商旅标准订单接口签名,通过main方法生成签名postman调试
+     */
+    private static String createTripSyncSign() {
+        //商旅订单标准接口报文,此处报文和实际接口入参报文存在差异,key值tripSync需要剔除,只需要传value值,且sign参数需要剔除
+        String str = "{\n" +
+                "        \"method\": \"er_planebill\",\n" +
+                "        \"service\": \"SY_SL\",\n" +
+                "        \"data\": [\n" +
+                "            {\n" +
+                "                \"billstatus\": \"A\",\n" +
+                "                \"ordernum\": \"DF210121121212212\",\n" +
+                "                \"pnr\": \"Zr3LF\",\n" +
+                "                \"ticketnum\": \"999-9000999000\",\n" +
+                "                \"parentordernum\": \"\",\n" +
+                "                \"oriordernum\": \"\",\n" +
+                "                \"overdesc\": \"\",\n" +
+                "                \"refundreason\": \"\",\n" +
+                "                \"changereason\": \"\",\n" +
+                "                \"changetype\": \"\",\n" +
+                "                \"fromcityname\": \"上海\",\n" +
+                "                \"takeoffportname\": \"SHA\",\n" +
+                "                \"tocityname\": \"广州\",\n" +
+                "                \"landingportname\": \"CAN\",\n" +
+                "                \"airlinename\": \"川航\",\n" +
+                "                \"flightno\": \"3U9911\",\n" +
+                "                \"cabin\": \"Q\",\n" +
+                "                \"tripid\": \"SHm4t\",\n" +
+                "                \"overflag\": \"\",\n" +
+                "                \"orderdate\": \"2024-01-31\",\n" +
+                "                \"totalamount\": \"858.62\",\n" +
+                "                \"ordertype\": \"O\",\n" +
+                "                \"isbusiness\": \"1\",\n" +
+                "                \"producttype\": \"1\",\n" +
+                "                \"orderstatus\": \"40000\",\n" +
+                "                \"ticketstatus\": \"UNUSED\",\n" +
+                "                \"ticketprice\": \"178.71\",\n" +
+                "                \"discount\": \"558.68\",\n" +
+                "                \"airportprice\": \"94.78\",\n" +
+                "                \"fuelprice\": \"318.53\",\n" +
+                "                \"tax\": \"233.28\",\n" +
+                "                \"refundamount\": \"\",\n" +
+                "                \"endorsementamount\": \"\",\n" +
+                "                \"assuranceamount\": \"\",\n" +
+                "                \"servicefee\": \"\",\n" +
+                "                \"standprice\": \"\",\n" +
+                "                \"ordersort\": \"1\",\n" +
+                "                \"orderstatusname\": \"\",\n" +
+                "                \"bookedname\": \"IatNP\",\n" +
+                "                \"travelername\": \"Nfwd9\",\n" +
+                "                \"operationtype\": \"1\",\n" +
+                "                \"choosenotminreason\": \"sDH6T\",\n" +
+                "                \"isapprove\": false,\n" +
+                "                \"cabinclass\": \"BDPLk\",\n" +
+                "                \"takeofftime\": \"2024-01-3109:45:05\",\n" +
+                "                \"landingtime\": \"2024-01-3109:45:05\",\n" +
+                "                \"isaddintegral\": false,\n" +
+                "                \"isdeductible\": true,\n" +
+                "                \"itinerarynum\": \"XoPV8\",\n" +
+                "                \"itinerarydate\": \"2024-01-31\",\n" +
+                "                \"downloadlink\": \"6aouW\",\n" +
+                "                \"happenddate\": \"2024-01-3109:45:05\",\n" +
+                "                \"lowestpirce\": \"524.76\",\n" +
+                "                \"islowestpirce\": false,\n" +
+                "                \"otheramount\": \"828.17\",\n" +
+                "                \"bookeddate\": \"2024-01-31\",\n" +
+                "                \"servicefeepaytype\": \"1\",\n" +
+                "                \"personalfee\": \"231.29\",\n" +
+                "                \"oabillformid\": \"er_tripreqbill\",\n" +
+                "                \"sourcebookedid\": \"qwe\",\n" +
+                "                \"expcommitcomnum\": 123,\n" +
+                "                \"expcommitdepnum\": 123,\n" +
+                "                \"sourcetravelerid\": \"qwe\",\n" +
+                "                \"travelerdept\": 123,\n" +
+                "                \"bookeddept\": 123,\n" +
+                "                \"company\": 123,\n" +
+                "                \"std_costcenter\": 123,\n" +
+                "                \"currency\": 123\n" +
+                "            }\n" +
+                "        ]\n" +
+                "    }";
+        Map map = JSONObject.parseObject(str, Map.class);
+        String result = genSign(map);
+        return String.format("sign:%s", getMD5Sign(result + SIT_FWS_KEY).toLowerCase(Locale.ROOT));
+    }
+
+
+    public static String genSign(Map<String, Object> params) {
+        params.remove("sign");
+        StringBuilder buf = new StringBuilder();
+
+        try {
+            List<Map.Entry<String, Object>> infoIds = new ArrayList(params.entrySet());
+            infoIds.sort(new Comparator<Map.Entry<String, Object>>() {
+                public int compare(Map.Entry<String, Object> o1, Map.Entry<String, Object> o2) {
+                    return ((String) o1.getKey()).compareTo((String) o2.getKey());
+                }
+            });
+            Iterator var3 = infoIds.iterator();
+
+            while (var3.hasNext()) {
+                Map.Entry<String, Object> item = (Map.Entry) var3.next();
+                if (item.getKey() != null && !((String) item.getKey()).isEmpty()) {
+                    String key = (String) item.getKey();
+                    String val = item.getValue() == null ? "" : item.getValue().toString();
+                    if (buf.toString().isEmpty()) {
+                        buf.append(key).append("=").append(val);
+                    } else {
+                        buf.append("&").append(key).append("=").append(val);
+                    }
+                }
+            }
+
+//            logger.info("商旅推送:生成签名前字符串:{}", buf.toString());
+        } catch (Exception e) {
+//            throw new KDBizException(String.format(ResManager.loadKDString("商旅集成,获取签名失败:%s", "TripCommonUtil_03", "fi-er-business", new Object[0]), var7));
+        }
+        return buf.toString();
+
+    }
+
+    public static String getMD5Sign(String input) {
+        StringBuilder sb = new StringBuilder(32);
+
+        try {
+            MessageDigest md5 = MessageDigest.getInstance("MD5");
+            if (input == null) {
+                throw new RuntimeException("md5 input string couldn't null");
+            }
+
+            md5.update(input.getBytes("utf-8"));
+            byte[] digest = md5.digest();
+            byte[] var4 = digest;
+            int var5 = digest.length;
+
+            for (int var6 = 0; var6 < var5; ++var6) {
+                byte d = var4[var6];
+                sb.append(hex(d));
+            }
+        } catch (NoSuchAlgorithmException var8) {
+//            throw new KDException(var8, new ErrorCode("md5 error", "not exist md5 instance"), new Object[0]);
+        } catch (UnsupportedEncodingException var9) {
+//            throw new RuntimeException(var9);
+        }
+
+        return sb.toString();
+    }
+
+    public static char[] hex(byte b) {
+        int low = b & 15;
+        int high = b >> 4 & 15;
+        char[] cs = new char[]{base[high], base[low]};
+        return cs;
+    }
+
+    private static final char[] base = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+
+}

+ 44 - 0
nckd-fi/src/main/java/nckd/fi/er/opplugin/AccommodationPushOpPlugin.java

@@ -0,0 +1,44 @@
+package nckd.fi.er.opplugin;
+
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.entity.plugin.AbstractOperationServicePlugIn;
+import kd.bos.entity.plugin.PreparePropertysEventArgs;
+import kd.bos.entity.plugin.args.EndOperationTransactionArgs;
+import nckd.base.common.utils.TripSyncBillUtils;
+import nckd.base.common.utils.TripSyncUtils;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * @description:住宿补助标准推送胜意系统,其他逻辑请写在其他类中
+ * @author: dingsixi
+ * @create: 2025/12/16 18:45
+ */
+public class AccommodationPushOpPlugin extends AbstractOperationServicePlugIn {
+    @Override
+    public void onPreparePropertys(PreparePropertysEventArgs e) {
+        super.onPreparePropertys(e);
+        e.getFieldKeys().addAll(this.billEntityType.getAllFields().keySet());
+    }
+
+    @Override
+    public void endOperationTransaction(EndOperationTransactionArgs e) {
+        super.endOperationTransaction(e);
+        DynamicObject[] dataEntities = e.getDataEntities();
+        //推送胜意创建国内酒店差旅标准
+        Arrays.stream(dataEntities).forEach(this::pushSYDHotelTravelStandards);
+    }
+
+    /**
+     * 推送胜意创建国内酒店差旅标准
+     *
+     * @param accommodation 住宿补助标准
+     */
+    private void pushSYDHotelTravelStandards(DynamicObject accommodation) {
+        //将住宿补助标准单据数据转换成接口的业务数据
+        Map<String, Object> data = TripSyncBillUtils.getAccommodationDataParam(accommodation);
+        //推送API
+        TripSyncUtils.pushApi("syncDHotelTravelStandards", Boolean.TRUE, data);
+    }
+}

+ 44 - 0
nckd-fi/src/main/java/nckd/fi/er/opplugin/TripAreaPushOpPlugin.java

@@ -0,0 +1,44 @@
+package nckd.fi.er.opplugin;
+
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.entity.plugin.AbstractOperationServicePlugIn;
+import kd.bos.entity.plugin.PreparePropertysEventArgs;
+import kd.bos.entity.plugin.args.EndOperationTransactionArgs;
+import nckd.base.common.utils.TripSyncBillUtils;
+import nckd.base.common.utils.TripSyncUtils;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * @description:出差地域推送胜意系统,其他逻辑请写在其他类中
+ * @author: dingsixi
+ * @create: 2025/12/16 16:18
+ */
+public class TripAreaPushOpPlugin extends AbstractOperationServicePlugIn {
+    @Override
+    public void onPreparePropertys(PreparePropertysEventArgs e) {
+        super.onPreparePropertys(e);
+        e.getFieldKeys().addAll(this.billEntityType.getAllFields().keySet());
+    }
+
+    @Override
+    public void endOperationTransaction(EndOperationTransactionArgs e) {
+        super.endOperationTransaction(e);
+        DynamicObject[] dataEntities = e.getDataEntities();
+        //推送胜意创建城市分类接口
+        Arrays.stream(dataEntities).forEach(this::pushSYCityType);
+    }
+
+    /**
+     * 推送胜意创建城市分类接口
+     *
+     * @param tripArea 出差地域
+     */
+    private void pushSYCityType(DynamicObject tripArea) {
+        //将出差地域单据数据转换成接口的业务数据
+        Map<String, Object> data = TripSyncBillUtils.getTripAreaBillDataParam(tripArea);
+        //推送API
+        TripSyncUtils.pushApi("syncCityClassificationCommon", Boolean.FALSE, data);
+    }
+}

+ 52 - 0
nckd-fi/src/main/java/nckd/fi/er/opplugin/VehicleStdPushOpPlugin.java

@@ -0,0 +1,52 @@
+package nckd.fi.er.opplugin;
+
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.entity.plugin.AbstractOperationServicePlugIn;
+import kd.bos.entity.plugin.PreparePropertysEventArgs;
+import kd.bos.entity.plugin.args.EndOperationTransactionArgs;
+import nckd.base.common.utils.TripSyncBillUtils;
+import nckd.base.common.utils.TripSyncUtils;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * @description:交通工具推送胜意系统,其他逻辑请写在其他类中
+ * @author: dingsixi
+ * @create: 2025/12/16 17:38
+ */
+public class VehicleStdPushOpPlugin extends AbstractOperationServicePlugIn {
+
+    @Override
+    public void onPreparePropertys(PreparePropertysEventArgs e) {
+        super.onPreparePropertys(e);
+        e.getFieldKeys().addAll(this.billEntityType.getAllFields().keySet());
+    }
+
+    @Override
+    public void endOperationTransaction(EndOperationTransactionArgs e) {
+        super.endOperationTransaction(e);
+        DynamicObject[] dataEntities = e.getDataEntities();
+        //推送胜意(新建国内机票差旅标准、新建火车票差旅标准)接口
+        Arrays.stream(dataEntities).forEach(this::pushSYVehicleStd);
+    }
+
+    /**
+     * 推送胜意(新建国内机票差旅标准、新建火车票差旅标准)接口
+     *
+     * @param vehicleStd 交通工具标准
+     */
+    private void pushSYVehicleStd(DynamicObject vehicleStd) {
+        //标准类型
+        String stdType = vehicleStd.getString("standardtype");
+        //只同步火车、机票
+        if (!(stdType.equals("train") || stdType.equals("air"))) {
+            return;
+        }
+
+        //将交通工具标准单据数据转换成接口的业务数据
+        Map<String, Object> data = TripSyncBillUtils.getVehicleStdDataParam(vehicleStd);
+        //推送API
+        TripSyncUtils.pushApi(stdType.equals("train") ? "syncTrainTravelStandards" : "syncDTicketTravelStandards", Boolean.TRUE, data);
+    }
+}