|
@@ -1,6 +1,7 @@
|
|
|
package com.etotem.cfc.service;
|
|
package com.etotem.cfc.service;
|
|
|
|
|
|
|
|
import com.alibaba.fastjson.JSON;
|
|
import com.alibaba.fastjson.JSON;
|
|
|
|
|
+import com.alibaba.fastjson.JSONArray;
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
import org.slf4j.LoggerFactory;
|
|
@@ -31,10 +32,12 @@ import java.net.HttpURLConnection;
|
|
|
import java.net.URL;
|
|
import java.net.URL;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.charset.StandardCharsets;
|
|
|
import java.util.Base64;
|
|
import java.util.Base64;
|
|
|
|
|
+import java.util.Date;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
import java.util.UUID;
|
|
import java.util.UUID;
|
|
|
import java.time.Duration;
|
|
import java.time.Duration;
|
|
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
|
|
|
|
|
@Service
|
|
@Service
|
|
|
public class WechatService implements WechatServiceInterface {
|
|
public class WechatService implements WechatServiceInterface {
|
|
@@ -69,6 +72,13 @@ public class WechatService implements WechatServiceInterface {
|
|
|
@Value("${wechat.env-version:release}")
|
|
@Value("${wechat.env-version:release}")
|
|
|
private String envVersion;
|
|
private String envVersion;
|
|
|
|
|
|
|
|
|
|
+ @Value("${wechat.mch-id:}")
|
|
|
|
|
+ private String mchId;
|
|
|
|
|
+
|
|
|
|
|
+ // 管理端发货后自动同步物流信息到微信「小程序交易管理服务」的开关(fail-open,默认开)
|
|
|
|
|
+ @Value("${wechat.fulfillment-enabled:true}")
|
|
|
|
|
+ private boolean fulfillmentEnabled;
|
|
|
|
|
+
|
|
|
private final RestTemplate restTemplate = createRestTemplate();
|
|
private final RestTemplate restTemplate = createRestTemplate();
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -641,4 +651,118 @@ public class WechatService implements WechatServiceInterface {
|
|
|
byte[] decrypted = cipher.doFinal(encryptedBytes);
|
|
byte[] decrypted = cipher.doFinal(encryptedBytes);
|
|
|
return new String(decrypted, StandardCharsets.UTF_8);
|
|
return new String(decrypted, StandardCharsets.UTF_8);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 是否启用「管理端发货 → 自动同步微信小程序交易管理服务」开关
|
|
|
|
|
+ */
|
|
|
|
|
+ public boolean isFulfillmentEnabled() {
|
|
|
|
|
+ return fulfillmentEnabled;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 快递公司中文名 → 微信物流公司编码映射(未命中透传原名)
|
|
|
|
|
+ */
|
|
|
|
|
+ public String mapExpressCompanyCode(String company) {
|
|
|
|
|
+ if (company == null || company.trim().isEmpty()) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ String c = company.trim();
|
|
|
|
|
+ if (c.contains("顺丰")) return "SF";
|
|
|
|
|
+ if (c.contains("中通")) return "ZTO";
|
|
|
|
|
+ if (c.contains("圆通")) return "YTO";
|
|
|
|
|
+ if (c.contains("韵达")) return "YD";
|
|
|
|
|
+ if (c.contains("申通")) return "STO";
|
|
|
|
|
+ if (c.contains("京东")) return "JD";
|
|
|
|
|
+ if (c.contains("EMS") || c.contains("邮政")) return "EMS";
|
|
|
|
|
+ return c;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信「小程序交易管理服务」发货信息录入 upload_shipping_info
|
|
|
|
|
+ * 管理端发货成功后调用,把物流信息同步到微信后台订单管理(用户微信侧收发货通知/查物流/确认收货)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * fail-open:失败只记日志返回错误信息,不阻塞本地发货流程;测试模式与未配置 mch-id 时跳过。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param outTradeNo 商户订单号(下单时 out_trade_no = 订单号,order_number_type=1 用 mchid+out_trade_no 匹配微信订单)
|
|
|
|
|
+ * @param buyerOpenid 买家 openid(payer.openid)
|
|
|
|
|
+ * @param trackingNo 物流单号
|
|
|
|
|
+ * @param expressCompany 快递公司(中文名,内部映射为微信编码;未命中透传)
|
|
|
|
|
+ * @param itemDesc 商品描述(≤120字,如 "商品名 x1")
|
|
|
|
|
+ * @return 成功返回 null;失败返回错误信息(不抛异常)
|
|
|
|
|
+ */
|
|
|
|
|
+ public String uploadShippingInfo(String outTradeNo, String buyerOpenid,
|
|
|
|
|
+ String trackingNo, String expressCompany, String itemDesc) {
|
|
|
|
|
+ if (!isFulfillmentEnabled()) {
|
|
|
|
|
+ log.info("发货同步微信已关闭(fulfillment-enabled=false),跳过 upload_shipping_info: orderNo={}", outTradeNo);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (testMode) {
|
|
|
|
|
+ log.info("测试模式:跳过 upload_shipping_info 同步微信: orderNo={}", outTradeNo);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (mchId == null || mchId.trim().isEmpty()) {
|
|
|
|
|
+ log.warn("wechat.mch-id 未配置,跳过 upload_shipping_info: orderNo={}", outTradeNo);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ String accessToken = getAccessToken();
|
|
|
|
|
+ String url = "https://api.weixin.qq.com/sec/order/upload_shipping_info?access_token=" + accessToken;
|
|
|
|
|
+
|
|
|
|
|
+ JSONObject orderKey = new JSONObject();
|
|
|
|
|
+ orderKey.put("order_number_type", 1); // 1=商户号+商户单号
|
|
|
|
|
+ orderKey.put("mchid", mchId);
|
|
|
|
|
+ orderKey.put("out_trade_no", outTradeNo);
|
|
|
|
|
+
|
|
|
|
|
+ JSONObject payer = new JSONObject();
|
|
|
|
|
+ payer.put("openid", buyerOpenid);
|
|
|
|
|
+
|
|
|
|
|
+ JSONObject shipping = new JSONObject();
|
|
|
|
|
+ shipping.put("tracking_no", trackingNo);
|
|
|
|
|
+ shipping.put("express_company", mapExpressCompanyCode(expressCompany));
|
|
|
|
|
+ shipping.put("item_desc", itemDesc == null ? "" : itemDesc);
|
|
|
|
|
+ JSONArray shippingList = new JSONArray();
|
|
|
|
|
+ shippingList.add(shipping);
|
|
|
|
|
+
|
|
|
|
|
+ JSONObject requestBody = new JSONObject();
|
|
|
|
|
+ requestBody.put("order_key", orderKey);
|
|
|
|
|
+ requestBody.put("logistics_type", 1); // 1=实体物流(自提/虚拟商品不调用)
|
|
|
|
|
+ requestBody.put("delivery_mode", 1); // 1=统一发货(2=分拆发货)
|
|
|
|
|
+ requestBody.put("upload_time", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").format(new Date()));
|
|
|
|
|
+ requestBody.put("payer", payer);
|
|
|
|
|
+ requestBody.put("shipping_list", shippingList);
|
|
|
|
|
+
|
|
|
|
|
+ org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
|
|
|
|
|
+ headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
|
|
|
|
|
+ org.springframework.http.HttpEntity<String> entity = new org.springframework.http.HttpEntity<>(
|
|
|
|
|
+ requestBody.toJSONString(), headers);
|
|
|
|
|
+
|
|
|
|
|
+ ResponseEntity<String> response = restTemplate.exchange(url,
|
|
|
|
|
+ org.springframework.http.HttpMethod.POST, entity, String.class);
|
|
|
|
|
+ JSONObject json = JSON.parseObject(response.getBody());
|
|
|
|
|
+ int errcode = json.getIntValue("errcode");
|
|
|
|
|
+ if (errcode == 0) {
|
|
|
|
|
+ // 顶层成功,但条目级可能部分失败(微信部分失败时顶层 errcode=0)
|
|
|
|
|
+ JSONArray list = json.getJSONArray("shipping_list");
|
|
|
|
|
+ if (list != null && !list.isEmpty()) {
|
|
|
|
|
+ for (int i = 0; i < list.size(); i++) {
|
|
|
|
|
+ JSONObject item = list.getJSONObject(i);
|
|
|
|
|
+ if (item != null && item.getIntValue("errcode") != 0) {
|
|
|
|
|
+ log.error("微信发货信息录入条目失败: orderNo={}, errcode={}, errmsg={}",
|
|
|
|
|
+ outTradeNo, item.getIntValue("errcode"), item.getString("errmsg"));
|
|
|
|
|
+ return "微信发货信息录入条目失败: " + item.getString("errmsg");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("微信发货信息录入成功: orderNo={}, trackingNo={}", outTradeNo, trackingNo);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ log.error("微信发货信息录入失败: orderNo={}, errcode={}, errmsg={}",
|
|
|
|
|
+ outTradeNo, errcode, json.getString("errmsg"));
|
|
|
|
|
+ return "微信发货信息录入失败: " + json.getString("errmsg");
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ // fail-open:失败只记日志,不阻塞本地发货
|
|
|
|
|
+ log.error("微信发货信息录入异常: orderNo={}, error={}", outTradeNo, e.getMessage(), e);
|
|
|
|
|
+ return "微信发货信息录入异常: " + e.getMessage();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|