文章目录
- 简介
- 名词解释
- 奇门对接方案
- 前期准备
- 系统调用流程
- 代码实现思路
- 关键点(个人观点)
- 奇门对接关键代码
- 可能遇到的问题
简介
- 淘宝奇门项目支持 ERP、WMS 之间的系统标准化对接,通过构建 ERP、WMS 系统之间标准通信协议来实现不同系统之间的打通;对商家来说,省去了更换系统软件所带来的额外开发成本;对 ISV(独立软件开发商) 来说,省去了与多家 ERP、WMS 系统对接难的问题,ERP 通过一次对接奇门项目,打通与所有 WMS 之间的通信,WMS 通过一次对接奇门项目,可以适配所有 ERP 软件;后期也将加入更多系统的支持,例如 CRM 与 ERP 的标准化对接,CRM 与在线订购类营销工具的标准化对接;
名词解释
奇门对接方案
无奇门的情况
- 目前商家使用的各个业务系统之间依靠 ISV 帮助实现 ERP 到 WMS 的对接,如果有多仓需求的商家还需要使用到 2 套以上的第三方仓储服务所提供的WMS 软件,ERP、WMS 各自对接,对接的总工作量为 N*N 倍,不但给 ISV的开发带来了极大的成本,对于后期维护,也将是一项艰巨的任务,如下图所示:
有奇门的情况
- 通过奇门项目后可使原有的网状对接结构变为一对一的对接方式,ERP、WMS 只需要与奇门数据总线对接一次即可完成所有系统的适配(特殊场景可能采用扩展字段的方式给与支持),如下图:
前期准备
- 首先要在淘宝开放平台上线对应的服务。
- 具体上线申请参考:应用接入流程 , 奇门仓储接入说明,奇门仓储标准白皮书
- 上线完成后,拿到对应开发语言的sdk,appkey,secret。
- 创建一个场景
- 点击进入。
- 然后就可以开始开发了。
系统调用流程
- 正向调用:前端 ERP 系统通过 TOP 接口与奇门项目应用进行交互,对于想要发送到 WMS 的请求首先发送到奇门应用,由奇门负责数据的解析、字段映射、数据翻译,再将处理后的数据通过 ERP 系统所请求的目的地发送至 WMS系统;WMS 系统收到请求后,将返回结果送回至奇门应用,由奇门应用统一返回至 ERP 系统;
- 反向调用:WMS 系统主动向 ERP 系统发出状态更新请求也是类似以上的访问步骤;
软件流程图:
代码实现思路
- 接收奇门主动请求接口收到的XML类型的参数,将XML转换为奇门定义的对象,然后将奇门对象转换为我们自己系统的对象。
- 判断请求的奇门接口名称走不同的业务实现方法,接口返回数据请参照淘宝奇门接口API文档
- 各实现方法返回Map格式数据,转换为xml格式返回。
关键点(个人观点)
- xml转为对象
- 对象转为xml
- 封装统一的接口响应
- 对应各个接口的实现和数据返回
奇门对接关键代码
@RestController
@RequestMapping("/qiMen")
@RequiredArgsConstructor
@Slf4j
public class QiMenController {
private final QiMenService qiMenService;
@ApiModelProperty("奇门调用WMS数据")
@RequestMapping(value="/apiRealization",produces="text/xml;charset=UTF-8")
public byte[] apiRealization(HttpServletRequest request, HttpServletResponse response) throws IOException {
byte[] result;
Map<String, Object> resultMap = new HashMap<>();
// API接口名称
String methodVal = request.getParameter("method");
// 验签
CheckResult checkResult = QiMenUtils.checkSign(request);
log.info("验签:{}",checkResult.isSuccess());
if(!checkResult.isSuccess()) {
resultMap.put("sub_code","sign-check-failure");
resultMap.put("sub_message","Illegal request");
resultMap.put("flag","failure");
result = QiMenUtils.multilayerMapToXml(resultMap, false).getBytes(StandardCharsets.UTF_8);
return result;
}
try {
// 解析xml参数
log.info("xml参数:{}", checkResult.getRequestBody());
JSONObject jsonObject = JSONUtil.xmlToJson(checkResult.getRequestBody());
log.info("jsonObject:{}", jsonObject);
if(Objects.nonNull(jsonObject)) {
if(!StringUtils.isBlank(methodVal)) {
switch (methodVal) {
case "entryorder.create":
// 入库单创建接口
resultMap = qiMenService.entryorderCreate(jsonObject);
break;
case "stockout.create":
// 出库单创建接口
resultMap = qiMenService.stockoutCreate(jsonObject);
break;
default:
resultMap = RCode.failure(RCode.FAILURE,"接口名称method填写有误");
break;
}
}else {
resultMap = RCode.failure(RCode.FAILURE,"接口名称method不能为空");
}
result = QiMenUtils.multilayerMapToXml(resultMap, false).getBytes("UTF-8");
}else {
result = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>".getBytes("UTF-8");
}
} catch (BizException e) {
resultMap = RCode.failure(RCode.FAILURE,e.getMessage());
try {
result = QiMenUtils.multilayerMapToXml(resultMap, false).getBytes("UTF-8");
} catch (UnsupportedEncodingException e1) {
return null;
}
} catch (Exception e) {
log.error("操作失败:" + e + "-----" + e.getMessage());
resultMap = RCode.failure(RCode.FAILURE,"操作失败");
try {
result = QiMenUtils.multilayerMapToXml(resultMap, false).getBytes("UTF-8");
} catch (UnsupportedEncodingException e1) {
return null;
}
}
log.info("ERP调用响应结果 resultMap:{}",resultMap);
return result;
}
@ApiModelProperty("WMS主动推送奇门数据")
@PostMapping("/push/{method}")
public Result pushQiMen(@PathVariable String method,@RequestBody JSONObject jsonObject) {
QimenResponse response = null;
switch (method) {
case "entryorder.confirm":
// 入库单确认
response = qiMenService.entryorderConfirm(jsonObject);
break;
case "stockout.confirm":
// 出库单确认
response = qiMenService.stockoutConfirm(jsonObject);
break;
default:
break;
}
return RCode.result(response);
}
}
@RequiredArgsConstructor
@Slf4j
@Service
public class QiMenServiceImpl implements QiMenService {
public static final String XML_KEY = "request";
/**
* 入库单创建
*
* @param jsonObject
* @return
*/
@Override
public Map<String, Object> entryorderCreate(JSONObject jsonObject) {
// 将请求头的数据转换为当前系统对应的dto类
EntryorderCreateRequest bean = jsonObject.getBean(XML_KEY, EntryorderCreateRequest.class);
List<JSONObject> list = jsonObject.getByPath("request.orderLines.orderLine", List.class);
if (CollUtil.isEmpty(list)) {
throw new BizException("入库单详情列表为空!");
}
List<EntryorderCreateRequest.OrderLine> orderLines = new ArrayList<>();
for (JSONObject object : list) {
orderLines.add(object.toBean(EntryorderCreateRequest.OrderLine.class));
}
bean.setOrderLines(orderLines);
QiMenReceiptOrderDTO receiptOrderDTO = QiMenConvert.entryorderCreate(bean);
// 远程调用创建入库单
Result result = qiMenRequestInBound.qiMenRequestInBound(receiptOrderDTO);
// 包装响应结果
return RCode.result(result);
}
/**
* WMS调用接口,回传入库单信息;
*
* @param jsonObject
* @return
*/
@Override
public QimenResponse entryorderConfirm(JSONObject jsonObject) {
// wms dto 转换为 EntryorderConfirmRequest
QiMenInboundAckVO bean = JSONUtil.toBean(jsonObject, QiMenInboundAckVO.class);
EntryorderConfirmRequest req = QiMenConvert.entryorderConfirm(bean);
req.setVersion("2.0");
req.setCustomerId("mockCustomerId"); // mockCustomerId 挡板测试的客户id
QimenClient client = getClient();
EntryorderConfirmResponse rsp = new EntryorderConfirmResponse();
try {
rsp = client.execute(req);
} catch (ApiException e) {
log.error("调用奇门异常:",e);
rsp.setMessage(e.getMessage());
}
return rsp;
}
public class QiMenUtils {
private final static String secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxx";
private final static String appkey = "xxxx";
// 联调地址 http://qimen.api.taobao.com/router/qmtest
// 正式地址 https://qimen.api.taobao.com/router/qimen/service
private final static String url = "https://qimen.api.taobao.com/router/qimen/service";
/**
* wms主动发起请求奇门的接口 创建一个连接
* @return
*/
public static QimenClient getClient() {
return new DefaultQimenClient(url, appkey, secret);
}
/**
* 验签 调用sdk中的签名验证方法
* @param request
* @return
*/
@SneakyThrows
public static CheckResult checkSign(HttpServletRequest request) {
return SpiUtils.checkSign(request, secret);
}
/**
* (多层)map转换为xml格式字符串
*
* @param map 需要转换为xml的map
* @param isCDATA 是否加入CDATA标识符 true:加入 false:不加入
* @return xml字符串
* @throws UnsupportedEncodingException
*/
public static String multilayerMapToXml(Map<String, Object> map, boolean isCDATA) throws UnsupportedEncodingException{
String parentName = "response";
Document doc = DocumentHelper.createDocument();
doc.addElement(parentName);
String xml = recursionMapToXml(doc.getRootElement(), parentName, map, isCDATA);
return formatXML(xml);
}
/**
* multilayerMapToXml核心方法,递归调用
*
* @param element 节点元素
* @param parentName 根元素属性名
* @param map 需要转换为xml的map
* @param isCDATA 是否加入CDATA标识符 true:加入 false:不加入
* @return xml字符串
*/
@SuppressWarnings("unchecked")
private static String recursionMapToXml(Element element, String parentName, Map<String, Object> map, boolean isCDATA) {
Element xmlElement = element.addElement(parentName);
map.keySet().forEach(key -> {
Object obj = map.get(key);
if (obj instanceof Map) {
recursionMapToXml(xmlElement, key, (Map<String, Object>)obj, isCDATA);
} else {
String value = obj == null ? "" : obj.toString();
if (isCDATA) {
xmlElement.addElement(key).addCDATA(value);
} else {
xmlElement.addElement(key).addText(value);
}
}
});
return xmlElement.asXML();
}
/**
* 格式化xml,显示为容易看的XML格式
*
* @param xml 需要格式化的xml字符串
* @return
*/
public static String formatXML(String xml) {
String requestXML = null;
try {
// 解析器
SAXReader reader = new SAXReader();
Document document = reader.read(new StringReader(xml));
if (null != document) {
StringWriter stringWriter = new StringWriter();
// 格式化,每一级前的空格
OutputFormat format = new OutputFormat("", true);
// xml声明与内容是否添加空行
format.setNewLineAfterDeclaration(false);
// 是否设置xml声明头部
format.setSuppressDeclaration(false);
// 是否分行
format.setNewlines(true);
XMLWriter writer = new XMLWriter(stringWriter, format);
writer.write(document);
writer.flush();
writer.close();
requestXML = stringWriter.getBuffer().toString();
}
return requestXML;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
可能遇到的问题
xml转换为对象
对象list转换为xml
你知道的越多,你不知道的越多。