diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/sfexpress/service/SFExpressApiService.java b/mall-shop/src/main/java/com/suisung/mall/shop/sfexpress/service/SFExpressApiService.java new file mode 100644 index 00000000..bce67130 --- /dev/null +++ b/mall-shop/src/main/java/com/suisung/mall/shop/sfexpress/service/SFExpressApiService.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2024. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. + * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. + * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. + * Vestibulum commodo. Ut rhoncus gravida arcu. + */ + +package com.suisung.mall.shop.sfexpress.service; + +import com.suisung.mall.common.pojo.res.SFExpressApiRes; + +import java.util.Map; + +public interface SFExpressApiService { + + /** + * (店铺)创建顺丰同城订单 + * + * @param shopOrderId 商家订单号 + * @return + */ + SFExpressApiRes createOrder(String shopOrderId); + + + /** + * 取消订单,当商家处发生异常需要取消配送时,可调用此接口对订单进行取消操作,同步返回结果。 + * + * @param sfOrderId 顺丰订单号 + * @param cancelCode 取消码,参考:https://openic.sf-express.com/open/api/docs/index/#/apidoc + * @param cancelReason 取消原因 + * @return + */ + SFExpressApiRes cancelOrder(String sfOrderId, Integer cancelCode, String cancelReason); + + + /** + * 取消订单,当商家处发生异常需要取消配送时,可调用此接口对订单进行取消操作,同步返回结果。 + * + * @param params 综合参数,顺丰订单号必填项,请参考:https://openic.sf-express.com/open/api/docs/index/#/apidoc + * @return + */ + SFExpressApiRes cancelOrder(Map params); + + /** + * 订单加小费,订单创建后,骑士未接单的情况下通过该接口对订单进行加小费,促进订单接单,截止订单完成前,都可以对订单加小费 + * + * @param params 综合参数,顺丰订单号order_id,订单小费 gratuity_fee,必填项,请参考:https://openic.sf-express.com/open/api/docs/index/#/apidoc + * @return + */ + SFExpressApiRes addOrderGratuityFee(Map params); + + /** + * 催单,当订单为配送状态中,可通过该接口发起催单 + * + * @param params 综合参数,顺丰订单号order_id必填项,请参考:https://openic.sf-express.com/open/api/docs/index/#/apidoc + * @return + */ + SFExpressApiRes reminderOrder(Map params); + + /** + * 获取配送员实时坐标接口,此接口用于获取订单配送员的实时经纬度坐标,一般情况下骑士经纬度30s更新一次。 + * + * @param params 综合参数,顺丰订单号order_id必填项,请参考:https://openic.sf-express.com/open/api/docs/index/#/apidoc + * @return + */ + SFExpressApiRes riderLatestPosition(Map params); + + /** + * 获取配送员轨迹H5,此接口可获取一个订单的骑士位置H5链接,可进行内嵌或发送给用户(内嵌时无法保证界面的兼容性,如发现兼容性问题可使用获取配送员坐标接口自行开发轨迹H5)。 + * + * @param params 综合参数,顺丰订单号order_id必填项,请参考:https://openic.sf-express.com/open/api/docs/index/#/apidoc + * @return + */ + SFExpressApiRes riderViewV2(Map params); + + /** + * 改单,支持店铺和企业客户改单,当订单生成后,可通过该接口修改收件人信息,可修改字段:收件地址、物品重量等,详情可参考请求参数列表 + * + * @param params 综合参数,请参考:https://openic.sf-express.com/open/api/docs/index/#/apidoc + * @return + */ + SFExpressApiRes changeOrder(Map params); + + // 顺丰同城回调相关业务 + + /** + * 接收顺丰原因订单取消回调 + * @param jsonData + * @param sign + * @return + */ + SFExpressApiRes receiveCancelOrderNotify(String jsonData, String sign); + + /** + * 接收顺丰配送状态更改回调 + * @param jsonData + * @param sign + * @return + */ + SFExpressApiRes receiveRiderOrderStatusNotify(String jsonData, String sign); + + /** + * 接收顺丰订单完成回调 + * @param jsonData + * @param sign + * @return + */ + SFExpressApiRes receiveOrderCompleteNotify(String jsonData, String sign); +} diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/sfexpress/service/impl/SFExpressApiServiceImpl.java b/mall-shop/src/main/java/com/suisung/mall/shop/sfexpress/service/impl/SFExpressApiServiceImpl.java new file mode 100644 index 00000000..08af72f8 --- /dev/null +++ b/mall-shop/src/main/java/com/suisung/mall/shop/sfexpress/service/impl/SFExpressApiServiceImpl.java @@ -0,0 +1,418 @@ +/* + * Copyright (c) 2024. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. + * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. + * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. + * Vestibulum commodo. Ut rhoncus gravida arcu. + */ + +package com.suisung.mall.shop.sfexpress.service.impl; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import com.suisung.mall.common.pojo.req.*; +import com.suisung.mall.common.pojo.res.SFExpressApiRes; +import com.suisung.mall.common.utils.JsonUtil; +import com.suisung.mall.shop.sfexpress.service.SFExpressApiService; +import org.apache.commons.codec.binary.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class SFExpressApiServiceImpl implements SFExpressApiService { + + private final static String sfExpressApiDomain = "https://openic.sf-express.com/open/api/external/"; + private static final Logger logger = LoggerFactory.getLogger(SFExpressApiServiceImpl.class); + @Value("${sf-express.appid}") + private Long appId; + @Value("${sf-express.appkey}") + private String appKey; + @Value("${sf-express.dev_id}") + private String devId; + + + + @Override + public SFExpressApiRes createOrder(String shopOrderId) { + // 组织请求参数 +// Map params = buildCommonParams(); +// params.put("app_id", appId); +// params.put("app_key", appKey); +// params.put("device_id", devId); + + Long now = DateUtil.currentSeconds(); + + SFCreateOrderReq param = new SFCreateOrderReq(); + param.setDev_id(1711573316); + param.setVersion(19); + param.setOrder_time(now); + param.setPush_time(now); + param.setRemark("测试顺丰同城发单,请不要通知骑手接单!"); + param.setOrder_sequence("000000123"); + param.setShop_id("3243279847393"); + param.setShop_order_id(shopOrderId); //"DD-20241118-00001" + param.setReturn_flag(511); + + SFOrderDetailReq orderDetail = new SFOrderDetailReq(); + orderDetail.setTotal_price(2000); + orderDetail.setProduct_type(6); + orderDetail.setWeight_gram(0); // 重量一律传 0kg 先,谢总本地协商好的 + orderDetail.setProduct_num(2); + orderDetail.setProduct_type_num(1); + + // 产品详情 + SFOrderProductDetailReq orderProductDetail = new SFOrderProductDetailReq(); + orderProductDetail.setProduct_id(1L); + orderProductDetail.setProduct_name("猪腿肉500g"); + orderProductDetail.setProduct_num(2); + + List orderProductList = new ArrayList<>(); + orderProductList.add(orderProductDetail); + + orderDetail.setProduct_detail(orderProductList); + + param.setOrder_detail(orderDetail); + + SFOrderShopReq shop = new SFOrderShopReq(); + shop.setShop_name("顺丰同城开放平台"); + shop.setShop_address("蜂巢工场西区"); + shop.setShop_phone("13203559287"); + shop.setShop_lng("116.327914"); + shop.setShop_lat("40.045488"); + param.setShop(shop); + + + SFOrderReceiveReq receive = new SFOrderReceiveReq(); + receive.setUser_name("顺丰同城"); + receive.setUser_phone("13881979410"); + receive.setUser_address("北京市海淀区学清嘉创大厦A座15层"); + receive.setUser_lng("116.352843"); + receive.setUser_lat("40.015028"); + param.setReceive(receive); + + + // 转换 json 字符串参数 + String paramJSON = JsonUtil.toJSONString(param); + + // 根据参数生成请求签名 + String send_url = buildUrl("createorder", paramJSON); + String retRespStr = HttpUtil.post(send_url, paramJSON); + if (StrUtil.isEmpty(retRespStr)) { + logger.error("顺丰同城:创建订单异常,无返回值!"); + return null; + } + + return JsonUtil.json2object(retRespStr, SFExpressApiRes.class); + } + + /** + * 取消订单 + * + * @param sfOrderId 顺丰订单号 + * @param cancelCode 取消码,参考:https://openic.sf-express.com/open/api/docs/index/#/apidoc + * @param cancelReason 取消原因 + * @return + */ + @Override + public SFExpressApiRes cancelOrder(String sfOrderId, Integer cancelCode, String cancelReason) { + Map params = buildCommonParams(); + return cancelOrder(params); + } + + /** + * 取消订单 + * + * @param params 综合参数,请参考:https://openic.sf-express.com/open/api/docs/index/#/apidoc + * @return + */ + @Override + public SFExpressApiRes cancelOrder(Map params) { + if (params == null || params.get("order_id") == null) { + return new SFExpressApiRes().fail(1003,"请求参数有误!"); + } + + // 转换 json 字符串参数 + String paramJSON = JsonUtil.toJSONString(params); + + // 根据参数生成请求签名 + String send_url = buildUrl("cancelorder", paramJSON); + String retRespStr = HttpUtil.post(send_url, paramJSON); + if (StrUtil.isEmpty(retRespStr)) { + logger.error("顺丰同城:取消订单异常,无返回值!"); + return new SFExpressApiRes().fail(-1,"顺丰同城:无返回值!"); + } + + return JsonUtil.json2object(retRespStr, SFExpressApiRes.class); + } + + /** + * 订单加小费,订单创建后,骑士未接单的情况下通过该接口对订单进行加小费,促进订单接单,截止订单完成前,都可以对订单加小费 + * + * @param params 综合参数,顺丰订单号order_id,订单小费 gratuity_fee,必填项,请参考:https://openic.sf-express.com/open/api/docs/index/#/apidoc + * @return + */ + @Override + public SFExpressApiRes addOrderGratuityFee(Map params) { + if (params == null || params.get("order_id") == null || params.get("gratuity_fee") == null) { + return new SFExpressApiRes().fail(1003,"请求参数有误!"); + } + + params.putAll(buildCommonParams()); + + // 转换 json 字符串参数 + String paramJSON = JsonUtil.toJSONString(params); + + // 根据参数生成请求签名 + String send_url = buildUrl("addordergratuityfee", paramJSON); + String retRespStr = HttpUtil.post(send_url, paramJSON); + if (StrUtil.isEmpty(retRespStr)) { + logger.error("顺丰同城:订单加小费,无返回值!"); + return new SFExpressApiRes().fail(-1,"顺丰同城:无返回值!"); + } + + return JsonUtil.json2object(retRespStr, SFExpressApiRes.class); + } + + /** + * 催单,当订单为配送状态中,可通过该接口发起催单 + * + * @param params 综合参数,顺丰订单号order_id必填项,请参考:https://openic.sf-express.com/open/api/docs/index/#/apidoc + * @return + */ + @Override + public SFExpressApiRes reminderOrder(Map params) { + if (params == null || params.get("order_id") == null) { + return new SFExpressApiRes().fail(1003,"请求参数有误!"); + } + + params.putAll(buildCommonParams()); + + // 转换 json 字符串参数 + String paramJSON = JsonUtil.toJSONString(params); + + // 根据参数生成请求签名 + String send_url = buildUrl("reminderorder", paramJSON); + String retRespStr = HttpUtil.post(send_url, paramJSON); + if (StrUtil.isEmpty(retRespStr)) { + logger.error("顺丰同城:催单异常,无返回值!"); + return new SFExpressApiRes().fail(-1,"顺丰同城:无返回值!"); + } + + return JsonUtil.json2object(retRespStr, SFExpressApiRes.class); + } + + /** + * 获取配送员实时坐标接口,此接口用于获取订单配送员的实时经纬度坐标,一般情况下骑士经纬度30s更新一次。 + * + * @param params 综合参数,顺丰订单号order_id必填项,请参考:https://openic.sf-express.com/open/api/docs/index/#/apidoc + * @return + */ + @Override + public SFExpressApiRes riderLatestPosition(Map params) { + if (params == null || params.get("order_id") == null) { + return new SFExpressApiRes().fail(1003,"请求参数有误!"); + } + + params.putAll(buildCommonParams()); + + // 转换 json 字符串参数 + String paramJSON = JsonUtil.toJSONString(params); + + // 根据参数生成请求签名 + String send_url = buildUrl("riderlatestposition", paramJSON); + String retRespStr = HttpUtil.post(send_url, paramJSON); + if (StrUtil.isEmpty(retRespStr)) { + logger.error("顺丰同城:获取配送员实时坐标异常,无返回值!"); + return new SFExpressApiRes().fail(-1,"顺丰同城:无返回值!"); + } + + return JsonUtil.json2object(retRespStr, SFExpressApiRes.class); + } + + /** + * 获取配送员轨迹H5,此接口可获取一个订单的骑士位置H5链接,可进行内嵌或发送给用户(内嵌时无法保证界面的兼容性,如发现兼容性问题可使用获取配送员坐标接口自行开发轨迹H5)。 + * + * @param params 综合参数,顺丰订单号order_id必填项,请参考:https://openic.sf-express.com/open/api/docs/index/#/apidoc + * @return + */ + @Override + public SFExpressApiRes riderViewV2(Map params) { + if (params == null || params.get("order_id") == null) { + return new SFExpressApiRes().fail(1003,"请求参数有误!"); + } + + params.putAll(buildCommonParams()); + + // 转换 json 字符串参数 + String paramJSON = JsonUtil.toJSONString(params); + + // 根据参数生成请求签名 + String send_url = buildUrl("riderviewv2", paramJSON); + String retRespStr = HttpUtil.post(send_url, paramJSON); + if (StrUtil.isEmpty(retRespStr)) { + logger.error("顺丰同城:获取配送员轨迹H5异常,无返回值!"); + return new SFExpressApiRes().fail(-1,"顺丰同城:无返回值!"); + } + + return JsonUtil.json2object(retRespStr, SFExpressApiRes.class); + } + + /** + * 改单,支持店铺和企业客户改单,当订单生成后,可通过该接口修改收件人信息,可修改字段:收件地址、物品重量等,详情可参考请求参数列表 + * + * @param params 综合参数,请参考:https://openic.sf-express.com/open/api/docs/index/#/apidoc + * @return + */ + @Override + public SFExpressApiRes changeOrder(Map params) { + return null; + } + + /** + * 接收顺丰原因订单取消回调 + * + * @param jsonData + * @param sign + * @return + */ + @Override + public SFExpressApiRes receiveCancelOrderNotify(String jsonData, String sign) { + if (StrUtil.isBlank(jsonData) || StrUtil.isBlank(sign)) { + return new SFExpressApiRes().fail(1003, "缺少必要参数!"); + } + + if (!checkOpenSign(sign, jsonData)) { + return new SFExpressApiRes().fail(2002, "请求签名sign校验失败!"); + } + + logger.info("接收顺丰原因订单取消回调返回的 JSON 数据:{}", jsonData); + + return new SFExpressApiRes().success("success"); + } + + /** + * 接收顺丰配送状态更改回调 + * + * @param jsonData + * @param sign + * @return + */ + @Override + public SFExpressApiRes receiveRiderOrderStatusNotify(String jsonData, String sign) { + if (StrUtil.isBlank(jsonData) || StrUtil.isBlank(sign)) { + return new SFExpressApiRes().fail(1003, "缺少必要参数!"); + } + + if (!checkOpenSign(sign, jsonData)) { + return new SFExpressApiRes().fail(2002, "请求签名sign校验失败!"); + } + + logger.info("接收顺丰原因订单取消回调返回的 JSON 数据:{}", jsonData); + + return new SFExpressApiRes().success("success"); + } + + /** + * 接收顺丰订单完成回调 + * + * @param jsonData + * @param sign + * @return + */ + @Override + public SFExpressApiRes receiveOrderCompleteNotify(String jsonData, String sign) { + if (StrUtil.isBlank(jsonData) || StrUtil.isBlank(sign)) { + return new SFExpressApiRes().fail(1003, "缺少必要参数!"); + } + + if (!checkOpenSign(sign, jsonData)) { + return new SFExpressApiRes().fail(2002, "请求签名sign校验失败!"); + } + + logger.info("接收顺丰原因订单取消回调返回的 JSON 数据:{}", jsonData); + + return new SFExpressApiRes().success("success"); + } + + + // 私有方法 + + /** + * 生成顺丰同城请求签名,参考官网:https://commit-openic.sf-express.com/#/apidoc + * + * @param postData + * @return + */ + private String generateOpenSign(String postData) { + try { + String sb = postData + + "&" + appId + "&" + appKey; + + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] md5 = md.digest(sb.getBytes(StandardCharsets.UTF_8)); + int i; + StringBuffer buf = new StringBuffer(); + for (byte b : md5) { + i = b; + if (i < 0) { + i += 256; + } + if (i < 16) { + buf.append("0"); + } + buf.append(Integer.toHexString(i)); + } + return Base64.encodeBase64String(buf.toString().getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + logger.error(e.getMessage()); + return ""; + } + } + + /** + * 验证签名是否一致 + * + * @param sign + * @param postData + * @return + */ + private boolean checkOpenSign(String sign, String postData) { + if (StrUtil.isBlank(sign) || StrUtil.isBlank(postData)) { + return false; + } + + String newSign = generateOpenSign(postData); + return sign.equals(newSign); + } + + private Map buildCommonParams() { + Map params = new HashMap<>(); + params.put("dev_id", devId); + params.put("push_time", DateUtil.currentSeconds()); + return params; + } + + /** + * 组装请求地址 + * + * @param urlPath + * @param postData + * @return + */ + private String buildUrl(String urlPath, String postData) { + String sb = sfExpressApiDomain + urlPath + + "?sign=" + + generateOpenSign(postData); + return sb; + } +}