Java对接拼多多开放平台API(加密上云等全流程)
前言
本文为【小小赫下士 blog】原创,搬运请保留本段,或请在醒目位置设置原文地址和原作者。
作者:小小赫下士
原文地址:Java对接拼多多开放平台API(加密上云等全流程)
本文章为企业ERP(ISV软件服务商)对接拼多多开放平台流程总结,文章包括开放平台入驻、商家授权接口流程、API调用流程(订单同步、发货回传以及其他接口的调用)、密文改造流程、拼多多电子面单调用流程、密文环境下发货流程、拼多多云服务以及服务入云流程。
(自研或者其他类型服务商请勿参考)
一、开放平台入驻
open平台地址:
注意事项:
1:手机号如果绑定为子账号不能再次注册开放平台账号:;
2:一个公司主体只能入驻一次,请谨慎选择入驻资质;
3:入驻资质暂时仅支持境内企业。
平台新手指南指导文档:
使用手机号注册,密码为英文加数字加特殊符号组合,注册好后跳转到首页。
点击立即入驻。
选择开发者角色为【电商软件服务商】,入驻组织资质勾选【企业开发者】。
开发者角色的认证业务选择【电商软件服务商】,开发者资质选择企业。
需要的资料如下图所示,按照要求填写上传。
完成后提交审核,然后可以在上方【工单支持】里发工单给平台技术支持人员,让平台同学加急审核一下。
审核完成后即可开始后续工作。
二、官方沟通工具Knock
资质申请好后先不要着急创建应用开始对接,先了解一下Knock的使用,这个工具是和官方工作人员沟通的一个途径(另一个途径是平台工单功能),平台更具不同的业务场景提供不同的官方群组,先准备好工具以及掌握使用可以提高后续的开发效率。
官方Knock介绍文档:
子账号管理指导文档:
三、创建应用
注意事项:
1:要求需要上传MRD文档(市场需求文档)以及PRD文档(产品需求说明书),创建应用页面会有模板提供下载,我自己保存的模板下载地址:
2:需要提供一个小于100M的视频,内容要求包括需要包含公司门头、职工办公场景,视频需要一镜到底,禁止剪辑。
【应用列表】进入【创建应用】,在【电商软件服务商】栏目下选择【企业ERP】。
官方企业ERP对接文档:
应用创建好后依旧需要审核,可以工单催审一下。
应用信息填写只需要注意回调地址一项,回调地址就是用来接收授权信息的服务接口地址,官方解释为:
应用创建好后,平台会根据应用权限集来提供Java开发SDK。
下载后自行配置到maven或者其他依赖管理工具里。
应用详情页会提供 client_id、client_secret 这两个应用秘钥,保存好并且不要泄露。
四、对接前准备
对接前请认真查阅以下官方提供的文档,遵守平台规则,切勿产生违规的行为:
- 官方应用开发安全管理规范文档地址:
- 收费规则(包括云外调用API以及云内调用的区别):
- 服务市场管理规范:
- 服务市场保证金规则:
- 违规处理规则:
- 企业ERP类目管理规范:
根据平台2021-04-13 11:44:11发布的规则变更公告【应用安全规范进一步改造通知】一文,要求如下:
官方改造指南文档地址:
新入驻的话可以直接按要求进行对应的开发,减少后续改造工作。
第一要求:确保敏感接口调用入云,也就是涉及到敏感数据的接口调用必须在拼多多云内调用,也就是需要购买拼多多平台的云主机服务,然后在云主机上去调用这些接口;
第二要求:如上图所示;
第三要求:规定不同的应用需要分别部署到不同的云资源上,这个其实是可以部署在同一个云主机上的,云数据库需要区分开。
第四要求和第五要求:没有特殊需求的话只需要使用设置IP白名单就可以。
官方列举的接口包括以下这些:
相关敏感接口(都是包含收件人信息的接口,敏感数据也就是买家收件人信息数据,虽然是密文...):
-
pdd.mall.info.get
-
pdd.order.information.get
-
pdd.order.list.get
-
pdd.order.number.list.get
-
pdd.order.number.list.increment.get
-
pdd.order.status.get
-
pdd.goods.information.get
-
pdd.goods.detail.get
-
pdd.goods.list.get
-
pdd.refund.address.list.get
-
pdd.refund.information.get
-
pdd.refund.list.increment.get
-
pdd.oversea.clearance.get
-
pdd.open.decrypt.batch
开发者需要确保应用对这些接口的调用是在多多云内发起。(开发测试除外)
需要订购的云服务有(一下推荐配置可根据实际业务程度按需升配或者降配):
1:云主机,服务部署使用,官方提供linux和windows两种,推荐使用windows系统。
规格推荐:2核4G,Windows Server 2008 R2 SP1 64位 服务器版,磁盘规格60GB,大概2460/年。
官方购买云主机指导文档:
2:云数据库,店铺授权后,配置好数据推送绑定,数据库会同步店铺的订单信息,后续需要在此数据库内获取店铺的订单数据,配合使用出云访问服务可以把订单数据发出云,本地可以开发一个接口接收数据,并进行后续的操作。
规格推荐:1核2000M,25G存储,大概2211/年。
官方购买云数据库指导文档:
平台云数据库的类型列表如下:
3:对象存储 OSS服务,部署使用,Windows 云主机需要通过对象存储 OSS 来上传与下载部署所需的程序包以及其他的文件,通过OSS下载云主机上的文件的时候是需要通过审核的。
OSS服务是按使用量每日计算费用,需要部署服务的时候需要用到,其他时间不计费,使用的当天大概也就几分钱。
官方指导文档:
4:出云访问服务 EGW,平台为了安全考虑,部署在云服务上的应用无法直接对外网发起访问,如果需要访问外网,比如公司的私有云服务,需要把订单数据发到公司本地的服务接口(刚需),需要使用EGW服务,推荐使用基础版。
EGW服务基础版也是按使用量收费。
其他服务按需订购使用,基础的对接开发使用以上四个服务足够。
本文为【小小赫下士 blog】原创,搬运请保留本段,或请在醒目位置设置原文地址和原作者。
作者:小小赫下士
原文地址:Java对接拼多多开放平台API(加密上云等全流程)
五、对接
0、拼多多密文规则
对接前请先熟知平台敏感信息加密策略,对应文档地址:
官方提供的企业ERP接入流程方案:
密文规则:
敏感字段列表
- card_info_list:卡号、卡密
- inner_transaction_id:支付申报订单号
- pay_no:支付单号
- receiver_name:收件人姓名
- receiver_phone:收件人电话
- receiver_address:收件人地址,不拼接省市区
- address:收件详细地址
- id_card_name:身份证姓名
- id_card_num:身份证号
例如原收件人姓名为 【张三】,则相关接口收件人姓名字段不在推送【张三】这个明文信息, 改为密文【~AgAAAAEj89wFUIsEOACIVyeI2r7XMNRS+DzOX5wSpiE=~j8+QVnFC~5~~】这种形式;
可通过下文的获取检索串工具来获取密文的检索串,不同订单但是收件人姓名相同则密文不同但是检索串相同,可通过检索串来进行判断合并订单等操作;
检索串样式【j8+QVnFC】;
然后可通过获取脱敏数据接口:
来获取密文的脱敏的数据,可以用来前端展示,或者报表、导出的文档展示使用,样式为【张*】
因为密文长度比较长,要把原数据库对应字段长度变更,并且需要新增两个用来存储检索串以及脱敏数据的字段。
而且密文改造后拼多多的订单要必须使用拼多多电子面单来取号打印发货,所以整个流转过程不需要明文信息,如果有特殊需求需要调用解密接口。
因为部分接口调用需要在云上调用的原因,这里提供的一个解决方案为,本地提供一个openAPI接口,云上部署一个调取订单数据以及发送到openAPI上的线程任务。
简单点来说就是:
本地服务器部署/开发用来接收云服务器请求的API接口,云服务器部署一个专门用来往接口发送数据的任务,其他业务操作在本地服务器执行,接口调用在云服务器执行。
下面会列举几个刚需的接口对接实现以及方案,实际对接可以参考一下。
1、授权
官方指导文档地址:
授权的具体流程:
客户角度:
客户在服务市场订购本ERP服务后,可进入平台给出的授权页面:
(或在系统使用也面自行添加授权按钮,自主组装授权页URL也是 )
(拼多多店铺web端授权页面示例)
(拼多多店铺h5端授权页面示例)
系统角度:
使用店铺账号授权后,授权码code将返回到回调地址中,以参数code形式组装至回调地址中,应用可以获取并使用该code去换取access_token(接口调用需要使用的调用令牌)。
例如:我们在应用详情页填写的回调地址为 http://www.erp.com:80/pddaccess
则授权后会以 http://www.erp.com:80/pddaccess?code=asdf123asdf123 的形式请求过来。
code授权码十分钟内有效,授权码可以用来访问获取调用令牌(access_token)的接口。
code有效期内多次使用code换取access_token是一样的。
简单接收授权码code
@Controller public class PddAccess { @GetMapping("/pddaccess") @ResponseBody public void PddRe(HttpServletRequest request,HttpServletResponse response){ try{ int contentlen = request.getContentLength(); String decode = ""; if(-1 != contentlen) { BufferedReader reader = request.getReader(); char[] buf = new char[contentlen]; int len = 0; StringBuffer contentBuffer = new StringBuffer(); while ((len = reader.read(buf)) != -1) { contentBuffer.append(buf, 0, len); } String content = contentBuffer.toString(); if (content == null) { content = ""; } decode = URLDecoder.decode(content, "UTF-8"); } String code = request.getParameter("code"); System.out.println(code); } catch (Exception e) { logger.error("拼多多授权异常:"+e.getMessage()); } } }
使用授权码code 获取访问接口获取调用令牌(access_token)
(以及使用获取到的access_token获取授权的店铺信息,以便维护,数据按需保存)
API文档地址:
获取店铺信息接口:
调用直接使用SDK封装好的方法,简单易用,每个API的文档下都有请求实例,几乎直接copy就可以。
// 应用的 client_id private String clientId = "1111111111"; // 应用的 client_secret private String clientSecret = "2222222222"; @Controller public class PddAccess { @GetMapping("/pddaccess") @ResponseBody public void PddRe(HttpServletRequest request,HttpServletResponse response){ try{ int contentlen = request.getContentLength(); String decode = ""; if(-1 != contentlen) { BufferedReader reader = request.getReader(); char[] buf = new char[contentlen]; int len = 0; StringBuffer contentBuffer = new StringBuffer(); while ((len = reader.read(buf)) != -1) { contentBuffer.append(buf, 0, len); } String content = contentBuffer.toString(); if (content == null) { content = ""; } decode = URLDecoder.decode(content, "UTF-8"); } String code = request.getParameter("code"); if(null != code && "".equals(code)){ PopClient client = new PopHttpClient(clientId, clientSecret); PddPopAuthTokenCreateRequest pddPopAuthTokenCreateRequest = new PddPopAuthTokenCreateRequest(); pddPopAuthTokenCreateRequest.setCode(code); PddPopAuthTokenCreateResponse pddPopAuthTokenCreateResponse = client.syncInvoke(pddPopAuthTokenCreateRequest); String ownerId = popAuthTokenCreateResponse.getOwnerId(); // 商家店铺id,店铺唯一标识 String ownerName = popAuthTokenCreateResponse.getOwnerName(); // 商家账号名称 String accessToken = popAuthTokenCreateResponse.getAccessToken(); // access_token String refreshToken = popAuthTokenCreateResponse.getRefreshToken(); // refresh token,可用来刷新access_token Integer expiresIn = popAuthTokenCreateResponse.getExpiresIn(); // access_token过期时间段,10(表示10秒后过期,现令牌过期时间已与订购时间相同) Date expiresDate = new Date(expiresIn * 1000); Integer refreshTokenExpiresIn = popAuthTokenCreateResponse.getRefreshTokenExpiresIn(); // refresh_token过期时间段,10表示10秒后过期 Date refreshTokenExpiresDate = new Date(refreshTokenExpiresIn * 1000); // 通过店铺信息接口获取店铺详细信息 PddMallInfoGetRequest pddMallInfoGetRequest = new PddMallInfoGetRequest(); PddMallInfoGetResponse pddMallInfoGetResponse = client.syncInvoke(pddMallInfoGetRequest, accessToken); PddMallInfoGetResponse.MallInfoGetResponse mallInfoGetResponse = pddMallInfoGetResponse.getMallInfoGetResponse(); String logo = mallInfoGetResponse.getLogo(); // 店铺logo图片链接 Integer mallCharacter = mallInfoGetResponse.getMallCharacter(); // 店铺身份,0:厂商 1:分销商 2:都不是 3:都是 String mallDesc = mallInfoGetResponse.getMallDesc(); // 店铺描述 Long mallId = mallInfoGetResponse.getMallId(); // 店铺id String mallName = mallInfoGetResponse.getMallName(); // 店铺名称 Integer merchantType = mallInfoGetResponse.getMerchantType(); // 店铺类型,1:个人 2:企业 3:旗舰店 4:专卖店 5:专营店 6:普通店 // 保存店铺信息(所有获取到的数据已在上方陈列,按需保存) // 保存步骤省略,根据业务需求自行保存 // 业务流转完成可以执行重定向到系统登录页面或系统使用地址 StringBuilder sb = new StringBuilder(); sb.append("<html>"); sb.append("<head>"); sb.append("</head>"); sb.append("<body>"); sb.append("订购成功,<a href=\"http://www.erp.com\">点击登陆</a>"); sb.append("</body>"); sb.append("</html>"); OutputStream outputStream = response.getOutputStream(); response.setHeader("content-type", "text/html;charset=UTF-8"); byte[] dataByteArr = sb.toString().getBytes("UTF-8"); outputStream.write(dataByteArr); return; } } catch (Exception e) { logger.error("拼多多授权异常:"+e.getMessage()); } } }
完整版(异常处理+系统内页面构建授权链接,自主组合拼接授权页面):
自主组合拼接授权页面流程:系统内页面添加点击授权超链接,点击后依然请求授权地址,通过请求是否携带code来判断是执行授权流程还是进入拼接平台授权页面流程。
平台给出的组装示例
https://{授权页链接}?response_type=code&client_id={应用client_id}&redirect_uri={client_id对应的回调地址}&state={自定义参数}
https://fuwu.pinduoduo.com/service-market/auth?response_type=code&client_id=4b6**********************672e4c9a&redirect_uri=https%3A%2F%2Fwww.oauth.net%2F2%2F&state=1212
要求组装的参数以及可组装的参数如下:
// 应用的 client_id private String clientId = "1111111111"; // 应用的 client_secret private String clientSecret = "2222222222"; @Controller public class PddAccess { @GetMapping("/pddaccess") @ResponseBody public void PddRe(HttpServletRequest request,HttpServletResponse response){ try{ int contentlen = request.getContentLength(); String decode = ""; if(-1 != contentlen) { BufferedReader reader = request.getReader(); char[] buf = new char[contentlen]; int len = 0; StringBuffer contentBuffer = new StringBuffer(); while ((len = reader.read(buf)) != -1) { contentBuffer.append(buf, 0, len); } String content = contentBuffer.toString(); if (content == null) { content = ""; } decode = URLDecoder.decode(content, "UTF-8"); } String code = request.getParameter("code"); if(null != code && "".equals(code)){ // 通过code授权码获取access_token PopClient client = new PopHttpClient(clientId, clientSecret); PddPopAuthTokenCreateRequest pddPopAuthTokenCreateRequest = new PddPopAuthTokenCreateRequest(); pddPopAuthTokenCreateRequest.setCode(code); PddPopAuthTokenCreateResponse pddPopAuthTokenCreateResponse = client.syncInvoke(pddPopAuthTokenCreateRequest); if(null == pddPopAuthTokenCreateResponse.getErrorResponse()){ PddPopAuthTokenCreateResponse.PopAuthTokenCreateResponse popAuthTokenCreateResponse = pddPopAuthTokenCreateResponse.getPopAuthTokenCreateResponse(); String ownerId = popAuthTokenCreateResponse.getOwnerId(); // 商家店铺id,店铺唯一标识 String ownerName = popAuthTokenCreateResponse.getOwnerName(); // 商家账号名称 String accessToken = popAuthTokenCreateResponse.getAccessToken(); // access_token String refreshToken = popAuthTokenCreateResponse.getRefreshToken(); // refresh token,可用来刷新access_token Integer expiresIn = popAuthTokenCreateResponse.getExpiresIn(); // access_token过期时间段,10(表示10秒后过期,现令牌过期时间已与订购时间相同) Date expiresDate = new Date(expiresIn * 1000); Integer refreshTokenExpiresIn = popAuthTokenCreateResponse.getRefreshTokenExpiresIn(); // refresh_token过期时间段,10表示10秒后过期 Date refreshTokenExpiresDate = new Date(refreshTokenExpiresIn * 1000); // 通过店铺信息接口获取店铺详细信息 PddMallInfoGetRequest pddMallInfoGetRequest = new PddMallInfoGetRequest(); PddMallInfoGetResponse pddMallInfoGetResponse = client.syncInvoke(pddMallInfoGetRequest, accessToken); if(null == pddMallInfoGetResponse.getErrorResponse()){ PddMallInfoGetResponse.MallInfoGetResponse mallInfoGetResponse = pddMallInfoGetResponse.getMallInfoGetResponse(); String logo = mallInfoGetResponse.getLogo(); // 店铺logo图片链接 Integer mallCharacter = mallInfoGetResponse.getMallCharacter(); // 店铺身份,0:厂商 1:分销商 2:都不是 3:都是 String mallDesc = mallInfoGetResponse.getMallDesc(); // 店铺描述 Long mallId = mallInfoGetResponse.getMallId(); // 店铺id String mallName = mallInfoGetResponse.getMallName(); // 店铺名称 Integer merchantType = mallInfoGetResponse.getMerchantType(); // 店铺类型,1:个人 2:企业 3:旗舰店 4:专卖店 5:专营店 6:普通店 // 保存店铺信息(所有获取到的数据已在上方陈列,按需保存) // 保存步骤省略,根据业务需求自行保存 // 业务流转完成可以执行重定向到系统登录页面或系统使用地址 StringBuilder sb = new StringBuilder(); sb.append("<html>"); sb.append("<head>"); sb.append("</head>"); sb.append("<body>"); sb.append("订购成功,<a href=\"http://www.erp.com\">点击登陆</a>"); sb.append("</body>"); sb.append("</html>"); OutputStream outputStream = response.getOutputStream(); response.setHeader("content-type", "text/html;charset=UTF-8"); byte[] dataByteArr = sb.toString().getBytes("UTF-8"); outputStream.write(dataByteArr); return; }else{ logger.error("拼多多店铺授权获取店铺信息异常,code = "+code+",error_msg = "+pddMallInfoGetResponse.getErrorResponse().getErrorMsg()); String error = toErrorHtmlNotice(returl, "授权异常,请联系ERP系统技术人员!"); OutputStream outputStream = response.getOutputStream(); response.setHeader("content-type", "text/html;charset=UTF-8"); byte[] dataByteArr = error.getBytes("UTF-8"); outputStream.write(dataByteArr); return; } }else{ logger.error("拼多多店铺授权获取AccessToken异常,code = "+code+",error_msg = "+pddPopAuthTokenCreateResponse.getErrorResponse().getErrorMsg()); String error = toErrorHtmlNotice(returl, "授权异常,请联系ERP系统技术人员!"); OutputStream outputStream = response.getOutputStream(); response.setHeader("content-type", "text/html;charset=UTF-8"); byte[] dataByteArr = error.getBytes("UTF-8"); outputStream.write(dataByteArr); return; } } // 如果请求不包含code参数,则执行以下授权页面跳转流程,引导商家进入授权页面 String returl = "https://fuwu.pinduoduo.com/service-market/auth?response_type=code&client_id=" + clientId + "&redirect_uri=" + URLEncoder.encode("http://www.erp.com/pddaccess", "utf-8"); returl = StringEscapeUtils.unescapeHtml(returl); if(null == request.getParameter("error")) { response.sendRedirect(returl); return; }else { switch (request.getParameter("error")) { default: response.sendRedirect(returl); break; case "invalid_client": StringBuilder sb = new StringBuilder(); sb.append("<script>"); sb.append("alert('您还没订购!点确定前往订购!');"); sb.append("window.location.href='"); sb.append(fuwuurl); // ERP应用的服务市场地址,跳转到此位置 sb.append("';"); sb.append("</script>"); OutputStream outputStream = response.getOutputStream(); response.setHeader("content-type", "text/html;charset=UTF-8"); byte[] dataByteArr = sb.toString().getBytes("UTF-8"); outputStream.write(dataByteArr); return; } } } catch (Exception e) { logger.error("拼多多授权异常:"+e.getMessage()); } } }
2、云工作台开启订单同步
只有开启订单同步服务后订购的云数据库才能同步到授权商家的订单信息,开启步骤参考下方订单同步使用手册文档。
- 订单同步服务产品介绍:
- 订单同步服务使用手册:
商家店铺授权完成后需要在商家管理里添加商家店铺ID,按照实际需要选择同步历史订单信息,添加完成,同步完成历史订单信息后云数据库就会实时的新增商家店铺的新订单信息。
后续同步商家订单信息只需要在云主机上查询云数据库内的数据并发送到我们设置好的外部API服务上即可。
3、云主机订单数据操作Java线程处理任务方案
为了接口API安全性,请自行配置加密策略或其他安全策略来接收传递相关数据,以下流程为直接传递(非安全)。
在线程处理任务程序中,查询云数据库内的订单列表,可以根据传统的时间段调用方式。
首次启动从两天前的时间点以半小时的时间段查询一次,然后把查询出的数据传递给API,API在本地对订单数据进行序列化存储到本地数据库内,查询可以根据订单更新时间 update_at 字段来判断,本地订单如果存在,并且更新时间一致则跳过,不一致并且本地存储的订单更新时间早于新的数据更新时间的话则把新的订单详情更新到本地数据库。
执行成功会返回请求结果信息,如果执行成功,则根据当前时间前进半个小时继续执行,以此类推,直到执行到当前时间,则以当前时间前二十分以及后十分钟为维度,慢慢查询遍历执行。
具体Java线程自动执行任务,可以根据实际系统配置进行开发,或自行寻找流程,这边不在展示架构。
云服务器部署的自动处理线程任务:
public class PddOrderSyncTask implements Runnable { // 记录开始时间 private Date lastjdpmodify = new Date(); // 记录最后更新时间 private Date newUpdateTime; @Override public void run() { pdptbTradeService = (IPdptbTradeService)applicationContext.getBean("PdptbTradeService"); while (true) { try { SyncPddOrderStateFrom(); } catch (Exception e) { e.printStackTrace(); } try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } } public void SyncPddOrderStateFrom() throws Exception { String url = "http://www.erp.com/pddOrderApi";// 远程API接口地址 Date nowdate = new Date(); Calendar calendar = Calendar.getInstance(); calendar.setTime(nowdate); // 时间推后一天 calendar.add(Calendar.DAY_OF_MONTH, -2); lastjdpmodify = calendar.getTime(); // 如果最后更新时间不为空,并且记录的开始时间小于记录的更新时间,则使用最后更新时间进行后续操作 if(newUpdateTime != null){ if (lastjdpmodify.getTime() <= newUpdateTime.getTime()) { Date needtime = new Date(newUpdateTime.getTime()); Calendar c = new GregorianCalendar(); c.setTime(needtime); c.add(Calendar.SECOND, -900); lastjdpmodify = c.getTime(); } } Date updateTime = lastjdpmodify; long timedetect = new Date().getTime() - updateTime.getTime(); int timeNumber = (int) (timedetect/(30*60*1000)); // 半个小时一个步伐,从记录的时间开始往当前时间遍历订单 // 例如当前时间为 2022-1-20 10:00:00,记录的时间lastjdpmodify = 2022-1-20 07:00:00 // 则订单更新时间判断开始时间为 2022-1-20 07:00:00,结束时间为 2022-1-20 07:30:00,以此类推,直到当前时间。 for(int k=0;k<=timeNumber;k++) { long startLong = updateTime.getTime()+k*30*60*1000; long endLong = startLong+30*60*1000; String start_date = sdf.format(startLong); String end_date = sdf.format(endLong); // 云数据库查询 // SELECT * FROM pdp_tb_trade where updated_at >= '${start_date}' updated_at <= '${end_date}' order by updated_at List<PdptbTradeInfo> pdptbTradeInfoList = pdptbTradeService.selectAll(start_date,end_date); if(pdptbTradeInfoList!=null && pdptbTradeInfoList.size()!=0){ Date updated_at = null; boolean sign = false; for (int i = 0; i < pdptbTradeInfoList.size(); i++) { PdptbTradeInfo pdptbTradeInfo = pdptbTradeInfoList.get(i); String param = JSONArray.toJSONString(pdptbTradeInfo); // 把订单数据发送到API接口上 String retpost = LoadJson(url,param); // 返回信息 if(retpost!=null){ PddResponse resp = JSONObject.parseObject(retpost, PddResponse.class); // 如果成功,更新 updated_at 订单更新时间记录 if(resp.getCode().equals("success")){ updated_at = pdptbTradeInfo.getUpdated_at(); sign=true; } // 如果失败,则不更新,并且从当前订单中断,重新遍历 if(resp.getCode().equals("error")){ logger.error(resp.getMessages()); sign=false; } }else{ break; } } if(updated_at!=null && sign){ newUpdateTime =updated_at; } if(!sign){ break; } } } } // post 请求 public String LoadJson(String url,String param){ StringBuilder json = new StringBuilder(); PrintWriter out = null; BufferedReader in = null; try { // Post请求的url,与get不同的是不需要带参数 URL urlload = new URL(url); HttpURLConnection connection = (HttpURLConnection) urlload.openConnection(); // 发送POST请求必须设置如下两行 connection.setDoOutput(true); connection.setDoInput(true); connection.setUseCaches(false); connection.setInstanceFollowRedirects(true); connection.setRequestMethod("POST"); // 设置请求方式 connection.setRequestProperty("Content-Type", "application/json"); // 设置接收数据的格式 connection.connect(); out = new PrintWriter(connection.getOutputStream()); out.print(param); // flush输出流的缓冲 out.flush(); in = new BufferedReader(new InputStreamReader(connection.getInputStream(),"UTF-8")); String inputLine = null; while ( (inputLine = in.readLine()) != null) { json.append(inputLine); } } catch (Exception e) { System.out.println("发送 POST 请求出现异常!" + e); return ""; } finally{ try{ if(out!=null){ out.close(); } if(in!=null){ in.close(); } } catch(IOException ex){ ex.printStackTrace(); } } return json.toString(); } }
本地服务器部署的API接收接口:
根据平台文档:
public class PddUtils { /** 拼多多敏感数据判断是否密文 **/ public static boolean isEncryptData(String data){ char SEP_PHONE = '$'; char SEP_ID = '#'; char SEP_NORMAL = '~'; Pattern BASE64_PATTERN = Pattern.compile("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"); if (null == data || data.length() < 44) { return false; } if (data.charAt(0) != data.charAt(data.length() - 1)) { return false; } char separator = data.charAt(0); String[] dataArray = StringUtils.split(data,separator); if (dataArray.length < 2 || !StringUtils.isNumeric(dataArray[dataArray.length - 1])) { return false; } if (separator == SEP_PHONE || separator == SEP_ID) { if (dataArray.length != 3) { return false; } if (data.charAt(data.length() - 2) == separator) { return BASE64_PATTERN.matcher(dataArray[0]).matches() && BASE64_PATTERN.matcher(dataArray[1]).matches() && dataArray[1].length()>=44; } else { return BASE64_PATTERN.matcher(dataArray[1]).matches() && dataArray[1].length()>=44; } } if (separator == SEP_NORMAL) { if (data.charAt(data.length() - 2) == separator) { if (dataArray.length != 3) { return false; } return BASE64_PATTERN.matcher(dataArray[0]).matches() && BASE64_PATTERN.matcher(dataArray[1]).matches() && dataArray[0].length()>=44; } else { if (dataArray.length != 2) { return false; } return BASE64_PATTERN.matcher(dataArray[0]).matches() && dataArray[0].length()>=44; } } return false; } /** 拼多多敏感数据获取密文检索串 **/ public static String extractIndex(String encryptedData) { if (encryptedData == null || encryptedData.length() < 4) { return null; } char sepInData = encryptedData.charAt(0); if (encryptedData.charAt(encryptedData.length() - 2) != sepInData) { return null; } String[] parts = StringUtils.split(encryptedData, sepInData); if (sepInData == '$' || sepInData == '#') { return parts[0]; } else { return parts[1]; } } }
通过接口获取订单敏感数据密文的脱敏数据(例:王*虎)
接口文档地址:
// 应用的 client_id private String clientId = "1111111111"; // 应用的 client_secret private String clientSecret = "2222222222"; @Service("PddEncryptionService") public class PddEncryptionServiceImpl implements IPddEncryptionService { // 密文脱敏 @Override public OrderInfo getDecryptMask(OrderInfo orderInfo) throws Exception { PopClient client = new PopHttpClient("http://gw-api.pinduoduo.com/api/router",clientId , clientSecret ); ShopInfo shopInfo = shopService.selectById(orderInfo.getShopid()); String accessToken = shopInfo.getSessionkey(); PddOpenDecryptMaskBatchRequest request = new PddOpenDecryptMaskBatchRequest(); List<PddOpenDecryptMaskBatchRequest.DataListItem> dataList = new ArrayList<PddOpenDecryptMaskBatchRequest.DataListItem>(); boolean success = true; //判断收货人 (不为空并且是密文) if(orderInfo.getReceivername()!=null && PddUtils.isEncryptData(orderInfo.getReceivername())){ PddOpenDecryptMaskBatchRequest.DataListItem item0 = new PddOpenDecryptMaskBatchRequest.DataListItem(); item0.setDataTag(orderInfo.getId()); item0.setEncryptedData(orderInfo.getReceivername()); dataList.add(item0); success = false; } //判断收货地址 (不为空并且是密文) if(orderInfo.getReceiveraddress()!=null && PddUtils.isEncryptData(orderInfo.getReceiveraddress())){ PddOpenDecryptMaskBatchRequest.DataListItem item1 = new PddOpenDecryptMaskBatchRequest.DataListItem(); item1.setDataTag(orderInfo.getId()); item1.setEncryptedData(orderInfo.getReceiveraddress()); dataList.add(item1); success = false; } //判断买家昵称 (不为空并且是密文) if(orderInfo.getBuyernick()!=null && PddUtils.isEncryptData(orderInfo.getBuyernick())){ PddOpenDecryptMaskBatchRequest.DataListItem item2 = new PddOpenDecryptMaskBatchRequest.DataListItem(); item2.setDataTag(orderInfo.getId()); item2.setEncryptedData(orderInfo.getBuyernick()); dataList.add(item2); success = false; } //判断手机号 (不为空并且是密文) if(orderInfo.getReceivermobile()!=null && PddUtils.isEncryptData(orderInfo.getReceivermobile())){ PddOpenDecryptMaskBatchRequest.DataListItem item3 = new PddOpenDecryptMaskBatchRequest.DataListItem(); item3.setDataTag(orderInfo.getId()); item3.setEncryptedData(orderInfo.getReceivermobile()); dataList.add(item3); success = false; } //判断电话 (不为空并且是密文) if(orderInfo.getReceiverphone()!=null && PddUtils.isEncryptData(orderInfo.getReceiverphone())){ PddOpenDecryptMaskBatchRequest.DataListItem item4 = new PddOpenDecryptMaskBatchRequest.DataListItem(); item4.setDataTag(orderInfo.getId()); item4.setEncryptedData(orderInfo.getReceiverphone()); dataList.add(item4); success = false; } if(success){ return orderInfo; } request.setDataList(dataList); PddOpenDecryptMaskBatchResponse response = client.syncInvoke(request, accessToken); PddOpenDecryptMaskBatchResponse.OpenDecryptMaskBatchResponse openDecryptMaskBatchResponse = response.getOpenDecryptMaskBatchResponse(); if (null == openDecryptMaskBatchResponse || null == openDecryptMaskBatchResponse.getDataDecryptList()){ return orderInfo; } List<PddOpenDecryptMaskBatchResponse.OpenDecryptMaskBatchResponseDataDecryptListItem> dataDecryptList = openDecryptMaskBatchResponse.getDataDecryptList(); for (int i = 0; i < dataDecryptList.size(); i++) { PddOpenDecryptMaskBatchResponse.OpenDecryptMaskBatchResponseDataDecryptListItem openListItem = dataDecryptList.get(i); if(openListItem.getEncryptedData().equals(orderInfo.getAlipayno()) && openListItem.getErrorCode() == 0){ orderInfo.setAlipaynomask(openListItem.getDecryptedData()); orderInfo.setAlipaynosearchstring(PddUtils.extractIndex(orderInfo.getAlipayno())); } if(openListItem.getEncryptedData().equals(orderInfo.getBuyernick()) && openListItem.getErrorCode() == 0){ String replace = openListItem.getDecryptedData().replace("'", "’").replace("/", ""); replace = filterOffUtf8Mb4(replace); if(StringUtils.isBlank(replace)){ replace = "未知"; } orderInfo.setBuyernickmask(replace); String s = PddUtils.extractIndex(orderInfo.getBuyernick()); orderInfo.setBuyernicksearchstring(s); } if(openListItem.getEncryptedData().equals(orderInfo.getReceivername()) && openListItem.getErrorCode() == 0){ String replace = openListItem.getDecryptedData().replace("'", "’").replace("/", ""); replace = filterOffUtf8Mb4(replace); if(StringUtils.isBlank(replace)){ replace = "未知"; } orderInfo.setReceivernamemask(replace); String s = PddUtils.extractIndex(orderInfo.getReceivername()); orderInfo.setNamesearchstring(s); } if(openListItem.getEncryptedData().equals(orderInfo.getReceiveraddress()) && openListItem.getErrorCode() == 0){ String replace = openListItem.getDecryptedData().replace("'", "’").replace("/", ""); replace = filterOffUtf8Mb4(replace); orderInfo.setReceiveraddressmask(replace); orderInfo.setAddresssearchstring(PddUtils.extractIndex(orderInfo.getReceiveraddress())); } if(openListItem.getEncryptedData().equals(orderInfo.getReceivermobile()) && openListItem.getErrorCode() == 0){ String replace = openListItem.getDecryptedData().replace("'", "’").replace("/", ""); replace = filterOffUtf8Mb4(replace); orderInfo.setReceivermobilemask(replace); String s = PddUtils.extractIndex(orderInfo.getReceivermobile()); orderInfo.setMobilesearchstring(s); } if(openListItem.getEncryptedData().equals(orderInfo.getReceiverphone()) && openListItem.getErrorCode() == 0){ String replace = openListItem.getDecryptedData().replace("'", "’").replace("/", ""); replace = filterOffUtf8Mb4(replace); orderInfo.setReceiverphonemask(replace); String s = PddUtils.extractIndex(orderInfo.getReceivermobile()); orderInfo.setPhonesearchstring(s); } } return orderInfo; } // 过滤特殊字符,emoji表情等 public String filterOffUtf8Mb4(String text) throws UnsupportedEncodingException { byte[] bytes = text.getBytes("utf-8"); ByteBuffer buffer = ByteBuffer.allocate(bytes.length); int i = 0; while (i < bytes.length) { short b = bytes[i]; if (b > 0) { buffer.put(bytes[i++]); continue; } b += 256; // 去掉符号位 if (((b >> 5) ^ 0x6) == 0) { buffer.put(bytes, i, 2); i += 2; } else if (((b >> 4) ^ 0xE) == 0) { buffer.put(bytes, i, 3); i += 3; } else if (((b >> 3) ^ 0x1E) == 0) { i += 4; } else if (((b >> 2) ^ 0x3E) == 0) { i += 5; } else if (((b >> 1) ^ 0x7E) == 0) { i += 6; } else { buffer.put(bytes[i++]); } } buffer.flip(); return new String(buffer.array(), "utf-8"); } }
业务处理接口:
@Controller public class PddOrderController{ @PostMapping("pddOrderApi") @ResponseBody public String PddRespon (HttpServletRequest request, HttpServletResponse response){ PddResponse resp = new PddResponse(); String plantform = "PDD"; try{ int contentlen = request.getContentLength(); BufferedReader reader = request.getReader(); char[] buf = new char[contentlen]; int len = 0; StringBuffer contentBuffer = new StringBuffer(); while ((len = reader.read(buf)) != -1) { contentBuffer.append(buf, 0, len); } String content = contentBuffer.toString(); if(content == null){ content = ""; } PdptbTradeInfo pdptbTradeInfo = JSONObject.parseObject(content, PdptbTradeInfo.class); //店铺id为"PDD"+mallid拼接而成 String shopid = plantform+pdptbTradeInfo.getMall_id(); //根据店铺id查询店铺是否存在或给以后的其他判断作准备 ShopInfo shopInfo = shopService.selectOneByShopCode(shopid); if(shopInfo == null){ resp.setCode("error"); resp.setMessages("[店铺【"+shopInfo.getShopname()+"】:不存在!]"); logger.error(resp.getMessages()); }else if(shopInfo.getExpiretime()==null || shopInfo.getExpiretime().before(new Date())){ resp.setCode("error"); resp.setMessages("[店铺【"+shopInfo.getShopname()+"】:已过期!]"); logger.error(resp.getMessages()); }else if(shopInfo.getShoptype()==0){ resp.setCode("error"); resp.setMessages("[店铺【"+shopInfo.getShopname()+"】:已关闭!]"); logger.error(resp.getMessages()); }else{ // 查询订单是否存在,如果存在并且新推来的订单更新时间小于或者等于本地存储的订单更新时间,则直接跳过 OrderInfo orderInfo = orderService.selectOrderInfoById(pdptbTradeInfo.getOrder_sn(), shopInfo.getId()); if(orderInfo!=null){ long a = orderInfo.getJdpmodified().getTime()/1000; long b = pdptbTradeInfo.getUpdated_at().getTime()/1000; if(a==b || a>b){ resp.setCode("success"); return JSONArray.toJSONString(resp); } } // 进入订单操作流程,存储或者修改本地订单状态等 String s = SyncDataToSql(shopInfo, pdptbTradeInfo); if(s.equals("success")){ resp.setCode("success"); } else if(s.equals("error")){ resp.setCode("error"); } } } catch (Exception e) { resp.setCode("error"); resp.setMessages("[发生异常]"); logger.error(e.getMessage()); e.printStackTrace(); } return JSONArray.toJSONString(resp); } // 订单操作流程,存储或者修改本地订单状态等 public String SyncDataToSql(ShopInfo shopInfo,PdptbTradeInfo pdptbTradeInfo) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 保存一份原始订单数据 PddOrderInfo pddOrderInfo = new PddOrderInfo(); String pdp_response = pdptbTradeInfo.getPdp_response(); JSONObject pddOrderItem = JSONObject.parseObject(pdp_response); pddOrderInfo.setId(pddOrderItem.getString("order_sn")); pddOrderInfo.setTenantid(shopInfo.getTenantid()); pddOrderInfo.setShopid(shopInfo.getId()); pddOrderInfo.setTidstr(pddOrderItem.getString("order_sn")); //1:待发货,2:已发货待签收,3:已签收 pddOrderInfo.setStatus(pddOrderItem.getString("order_status")); pddOrderInfo.setSellernick(shopInfo.getShopnick()); String remark = pddOrderItem.getString("remark"); String buyer_memo = pddOrderItem.getString("buyer_memo"); String receiver_name = pddOrderItem.getString("receiver_name"); String address = pddOrderItem.getString("address"); String receiver_address = pddOrderItem.getString("receiver_address"); try{ if(StringUtils.isNotBlank(remark)){ remark = pddOrderItem.getString("remark").replace("'","’").replace("/","//"); remark = filterOffUtf8Mb4(remark); } if(StringUtils.isNotBlank(buyer_memo)){ buyer_memo = pddOrderItem.getString("buyer_memo").replace("'","’").replace("/","//"); buyer_memo = filterOffUtf8Mb4(buyer_memo); } String item_list_str = pddOrderItem.getString("item_list"); JSONArray itemList = JSONArray.parseArray(item_list_str); for(int j=0;j<itemList.size();j++) { JSONObject orderStock = itemList.getJSONObject(j); if(StringUtils.isNotBlank(orderStock.getString("goods_name"))){ String goods_name = orderStock.getString("goods_name").replace("'", "’").replace("/", "//"); orderStock.remove("goods_name"); orderStock.put("goods_name",goods_name); } } }catch (Exception e){ e.printStackTrace(); } pddOrderItem.remove("receiver_name"); pddOrderItem.put("receiver_name",receiver_name); pddOrderItem.remove("remark"); pddOrderItem.put("remark",remark); pddOrderItem.remove("buyer_memo"); pddOrderItem.put("buyer_memo",buyer_memo); pddOrderItem.remove("receiver_address"); pddOrderItem.put("receiver_address",receiver_address); pddOrderItem.remove("address"); pddOrderItem.put("address",address); pddOrderInfo.setCreated(pddOrderItem.getString("created_time")); pddOrderInfo.setModified(pddOrderItem.getString("updated_at")); pddOrderInfo.setJdpcreated(pddOrderItem.getString("created_time")); pddOrderInfo.setJdpmodified(pddOrderItem.getString("updated_at")); pddOrderInfo.setJdpresponse(pddOrderItem.toString().replace("'","''")); pddOrderService.createOrUpdate(pddOrderInfo); //判断订单是否为平台风控订单 如果是平台风控订单 直接跳过 if (StringUtils.isNotBlank(pddOrderItem.getString("risk_control_status")) && pddOrderItem.getString("risk_control_status").equals("1")){ logger.error("拼多多订单号:"+pddOrderItem.getString("order_sn")+"为平台风控订单跳过该订单 等平台审核完毕"); return "success"; } // 开始保存到本地或者更改本地订单状态 // 此步骤省略。。。。。。。。。。。。 // 大致流程为序列化为实体类,然后调用工具类的获取检索串工具以及调用通过密文获取脱敏数据接口来获取对应的检索串以及脱敏数据 // 然后中间流程需要根据订单状态 order_status 字段来判断订单有没有发生退款售后等 return "success"; } }
3、订单发货通知接口(回传物流单号/回传发货状态)
流程与上面的订单不同流程一直,依然是一个部署在拼多多云服务器上的自动处理任务,以及一个部署在本地的接口,云主机上的任务先去接口获取需要发货回传的订单信息,然后自动处理任务根据订单信息调用【订单发货通知接口】,进行发货回传。
订单发货通知接口文档地址:
获取快递公司编码调用快递公司查看接口:
自动发货回传处理任务:
public class PddStateSendTask implements Runnable { // 应用的 client_id private String clientId = "1111111111"; // 应用的 client_secret private String clientSecret = "2222222222"; @Override public void run(){ while (true) { try { SyncPddStorageStateFromApi(); } catch (Exception e) { e.printStackTrace(); } try { Thread.sleep(12 * 1000); } catch (Exception e) { e.printStackTrace(); } } } public void SyncPddStorageStateFromApi()throws Exception { try { BooleanReason reason = DeliveryDStateSync(商家授权令牌, 订单ID, 物流单号, 快递公司编码); if(reason.getValue()){ logger.error("拼多多发货接口操作成功:" + item.getId()); storageSendState = "1"; }else{ logger.error("拼多多发货接口返回错误:" + item.getId() + "," + reason.getReasons().get(0)); storageSendState = "2"; if (reason.getReasons().get(0).contains("订单已发货") || reason.getReasons().get(0).contains("订单已签收")) { storageSendState = "1"; } } } catch (Exception e) { e.printStackTrace(); logger.error("拼多多发货接口调用异常:" + e.getMessage()); storageSendState = "2"; } Thread.sleep(2000); // 此位置修改订单发货状态为发货成功或者发货失败 } // 调用发货通知接口 public BooleanReason DeliveryDStateSync(String access, String order_sn, String tracking_number, Long logistics_id) throws Exception { BooleanReason reason = new BooleanReason(); reason.setValue(false); PopClient client = new PopHttpClient(clientId, clientSecret); PddLogisticsOnlineSendRequest request = new PddLogisticsOnlineSendRequest(); request.setLogisticsId(logistics_id); request.setOrderSn(order_sn); request.setTrackingNumber(tracking_number); PddLogisticsOnlineSendResponse response = client.syncInvoke(request, access); if(null == response.getErrorResponse()){ reason.setValue(true); }else{ reason.addReason(response.getErrorResponse().getErrorMsg()); } return reason; } }
六、拼多多电子面单
1、接入前准备工作
因为密文的原因,拼多多的订单必须要使用拼多多电子面单系统进行取单打印发货(拼多多电子面单支持直接入参密文来取号以及打印)。
官方单子面单接入指南文档:
首先引导商家前往店铺后台开通电子面单服务,绑定月结卡号或者网点信息,等待物流商审核通过,并且确保已充值上面单余额。 (如果ERP支持多店铺管理,同一商家多个拼多多店铺的话,可以只需要一个店铺开通就行,其他店铺可共用这个店铺的电子面单)
引导地址:
2、电子面单自定义区域设置
开通成功后引导商家前往拼多多面单编辑器系统:
自定义区模板就是打印的面单最下面一部分可以自由编辑的区域。
一般在此位置展示的是订单号,商品编码/数量,以及其他作业相关的序列号,波次号等。
新建与开通的物流服务商对应的电子面大模板,并且编辑商家自定义区。
需要提供给商家自定义区的参数以及位置排版,此方式需要提前引导商家制作自定义区模板。
参数格式是 <%=data.订单号%>,可以支持中文,data.字段名,比如需要展示订单号,可以在服务端封装好 ["订单号":orderid],下文有详细步骤。
自定义区模板也可以在开放平台ISV预设置好模板,然后在打印时获取需要展示的信息进行打印即可。系统商家共同使用开放平台设置好的模板,这种方式统一度比较高。
3、打印组件
拼多多电子面单打印需要使用拼多多电子面单打印组件,系统提供打印功能,商家在需要作业的电脑上安装打印组件并且设置好默认的打印机,然后就可通过系统来操作打印了。
拼多多打印组件官方下载地址:
官方打印组件交互协议指导文档(下文到打印流程会详细介绍):
4、获取快递单号
获取快递单号支持使用密文入参。
因为平台面单号支持自动回收,所以没有特殊情况不需要对接取消单号的流程,而且可以设置自动任务,把需要发货的订单自动获取快递单号。
查看快递公司列表:
用来获取拼多多侧的物流名称和物流编码。
接口:pdd.logistics.companies.get
获取所有标准电子面单模板
接口:
获取商家的自定义区模板信息
接口:
获取电子面单打印信息包括快递单号
接口:
打印组件交互以及打印示例:
JSONArray docu = new JSONArray(); StringBuilder codeStr = new StringBuilder(); List<OrderStockInfo>orderStockInfoList = orderStockService.selectByOrderCode(orderVo.getOrdercode()); for (OrderStockInfo orderStockInfo : orderStockInfoList) { codeStr.append("条码:").append(orderStockInfo.getItemcode()).append(";数量:").append(orderStockInfo.getNum()).append("\n"); } JSONObject backDataJSON = JSON.parseObject(print_data); // 电子面单打印信息 返回值中的 print_data // 自定义区的显示内容 JSONObject dataJSON = new JSONObject(); dataJSON.put("订单号","1111111"); dataJSON.put("序号",1); dataJSON.put("备货任务单号",2222); dataJSON.put("数量",1); dataJSON.put("条码",3333333); JSONObject cusJSON = new JSONObject(); cusJSON.put("data",dataJSON); cusJSON.put("templateURL",custom_area_url); // 商家自定义区模板链接 CnCloudPrintDocuments documents = new CnCloudPrintDocuments(); documents.setDocumentID(waybill_code); //文档唯一ID,拼多多标准面单建议使用面单号 JSONArray contents = new JSONArray(); contents.add(JSON.toJSON(backDataJSON)); contents.add(JSON.toJSON(cusJSON)); documents.setContents(contents); docu.add(documents); try { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSS"); String taskid = sdf.format(new Date()); String uri = "ws://127.0.0.1:5000"; //拼多多打印组件Websocket地址:ws://127.0.0.1:5000 WebSocketClientManager websocket = new WebSocketClientManager(new URI(uri),new Draft_6455()); websocket.connect(); PddCloudPrintVo pddCloudPrintVo = new PddCloudPrintVo(); PddCloudPrintVo.task task = new PddCloudPrintVo.task(); task.setTaskID(taskid); task.setPrinter(printername); // 打印机名称 task.setDocuments(docu); // 打印预览 // task.setPreview("true"); // task.setPreviewType("image"); pddCloudPrintVo.setTask(JSON.toJSON(task)); pddCloudPrintVo.setCmd("print"); pddCloudPrintVo.setRequestID(taskid); pddCloudPrintVo.setVersion("1.0"); while (!websocket.getReadyState().equals(ReadyState.OPEN)) { try{ Thread.sleep(1000); } catch(Exception e){ e.printStackTrace(); } // logger.debug("连接中···请稍后"); } websocket.send(JsonUtils.toJson(pddCloudPrintVo)); }catch (URISyntaxException url){ url.printStackTrace(); }
时间仓促,如有错误欢迎指出,欢迎在评论区讨论,如对您有帮助还请点个推荐、关注支持一下
作者:博客园 - 凉年技术
出处:http://www.cnblogs.com/xxhxs-21/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文链接,否则保留追究法律责任的权利。
若内容有侵犯您权益的地方,请公告栏处联系本人,本人定积极配合处理解决。