微信支付服务端自动对账
支付及对账流程链接
自动对账说明
微信支付后每天的商户系统的自动对账还是比较繁琐的,所谓自动对账实际上就是将商户某天交易成功的订单信息与微信某天的账单进行逐一匹配的过程。其涉及到三个专业名词短款、长款和金额不一致,以下对其进行解释:
**短款:**平台资金流有,而支付公司没有的差异,可能是因为支付公司日切早于平台的账务,一旦出现,会出现短款的现象,银行日切导致段差异,会在下一天与银行的勾对中,将此笔差异勾对上,如果是非日切导致的原因,就需要深入追查或让支付公司补款了
**长款:**平台资金流没有,而第三方支付有的差异。可能原因如下:
1)第三方支付日切晚于平台订单账务;
2)平台处理订单时的掉单。一旦出现,则会出现长款(即银行不应该结算而实际结算)的现象,对于因日切导致的差异,在第二天的对账中系统会自动解决,其他原因的,需要技术排查。
**金额不一致:**本地已支付,支付渠道已支付,但是金额不同,这个需要人工来逐一核查。
注意:微信后台是每天九点会生成前一天的账单,故商户后台处理对账下载账单逻辑时建议定时任务定在每天十天再去处理
本人所写自动对账JAVA代码
public void downloadWechatBill() throws Exception {
cachedThreadPool.execute(() -> {
try{
Calendar ca = Calendar.getInstance();
ca.setTime(new Date());
ca.add(Calendar.DATE, -1);
SimpleDateFormat dateFormat= new SimpleDateFormat("yyyyMMdd");
SimpleDateFormat dateFormatStr= new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat dateFormatTime= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Map<Object, Object> map = new HashMap<>();
map.put("appid", APPID);
map.put("mch_id", MCHID);
map.put("nonce_str", RandomUtil.getRandomStringByLength(32));
map.put("bill_date", dateFormat.format(ca.getTime()));
map.put("bill_type", "ALL");
map.put("tar_type", "GZIP");
//根据算法生成签名
String sign = Md5EncryptUtil.getWechatSign(map, KEY_APPSECRET);
map.put("sign", sign);
//将参数转换成xml
String paramXML = XMLParser.converterPayPalm(map);
// 下载文件
LOG.info("【微信对账业务】-开始下载账单");
Boolean downloadSuccess = downloadFile(DOWNLOADB_BILL_URL, FileCatalogEnum.WECHAT_DOWNLOAD_BILL_FILE.getType()+dateFormat.format(ca.getTime())+".gzip",paramXML);
if(downloadSuccess){
LOG.info("【微信对账业务】-下载账单成功");
//如所传日期有账单并下载成功,刚将其上传至ftp服务器做备份
FileInputStream in=new FileInputStream(new File(FileCatalogEnum.WECHAT_DOWNLOAD_BILL_FILE.getType()+dateFormat.format(ca.getTime())+".gzip"));
uploadFile(FILE_FTP_HOST, Integer.valueOf(FILE_FTP_PORT), FILE_FTP_USERNAME, FILE_FTP_PASSWORD, FileCatalogEnum.WECHAT_DOWNLOAD_BILL_FILE.getType(), dateFormat.format(ca.getTime())+".gzip", in);
//从服务器所存储账单的路径下读取账单信息并开始对账服务
List<String[]> billInfoArr = this.readGzipBillInfo(FileCatalogEnum.WECHAT_DOWNLOAD_BILL_FILE.getType()+dateFormat.format(ca.getTime())+".gzip");
List<Map<String, Object>> saveDiffShortList = new ArrayList<>();
List<Map<String, Object>> saveDiffLongList = new ArrayList<>();
if(billInfoArr == null){
LOG.info("【微信对账业务】-总金额相等无须逐一对账");
//无须对账将服务器中的文件删除
this.deleteServerFile(FileCatalogEnum.WECHAT_DOWNLOAD_BILL_FILE.getType()+dateFormat.format(ca.getTime())+".gzip");
return;
}
if(billInfoArr.size()>0){
//此处为对账业务逻辑处理
//获取下载账单日期的商户后台的当日订单
List<PaymentTradePO> paymentTradePOList = iWechatPayDao.getOrderByBillDate(dateFormatStr.format(ca.getTime()));
LOG.info("【微信对账业务】-开始对账");
//首先以商户后台的订单为主去比对微信的订单,如商户有订单而微信没有则标记为短款,如两者都有但金额不一致则标记为金额不一致
for(PaymentTradePO tradePO : paymentTradePOList){
int count = 1;
String tradeId = tradePO.getTradeId();
String account = tradePO.getPrice().setScale(2, BigDecimal.ROUND_HALF_UP).toString();
for(String[] strArr : billInfoArr){
Date tradeTime = dateFormatTime.parse(strArr[0]);
String appid = strArr[1];
String mchid = strArr[2];
String wechatOrderId = strArr[5];
String wechatTradeId = strArr[6];
String wechatAccount = strArr[12];
if(tradeId.equals(wechatTradeId) && account.equals(wechatAccount)){
billInfoArr.remove(count-1);
break;
}
//出现金额不一致的情况
else if(tradeId.equals(wechatTradeId) && !account.equals(wechatAccount)){
WechatCheckAccountOrderDiffPO diffPO = new WechatCheckAccountOrderDiffPO(appid,mchid);
diffPO.setOrderDiffId(LogicIdUtil.bussinessId());
diffPO.setBussinessOrderNum(wechatTradeId);
diffPO.setWechatOrderNum(wechatOrderId);
diffPO.setOrderTime(tradePO.getCreateTime());
diffPO.setTradeSuccessTime(tradeTime);
diffPO.setWechatTradeAmount(new BigDecimal(wechatAccount));
diffPO.setBussinessTradeAmount(tradePO.getPrice());
diffPO.setDiffType(WechatCheckBilDiffTypeEnum.INCONSISTENT_AMOUNT.getType());
diffPO.setDiffOrderStatus(WechatCheckBilDiffStatusEnum.NOT_FLAT_ACCOUNT.getType());
diffPO.setStatus(DataStatusEnum.NORMAL.getType());
diffPO.setCreateTime(new Date());
saveDiffShortList.add(SqlUtil.durableData(diffPO, PlatformConstants.TABLE_SAVE));
break;
}
if(count==billInfoArr.size()){
//此时就出现短款
if(!tradeId.equals(wechatTradeId)){
WechatCheckAccountOrderDiffPO diffPO = new WechatCheckAccountOrderDiffPO(appid,mchid);
diffPO.setOrderDiffId(LogicIdUtil.bussinessId());
diffPO.setBussinessOrderNum(tradeId);
diffPO.setWechatOrderNum(tradePO.getTransactionId());
diffPO.setOrderTime(tradePO.getCreateTime());
diffPO.setTradeSuccessTime(tradePO.getUpdateTime());
diffPO.setBussinessTradeAmount(tradePO.getPrice());
diffPO.setDiffType(WechatCheckBilDiffTypeEnum.SHORT_PARAGRAPH.getType());
diffPO.setDiffOrderStatus(WechatCheckBilDiffStatusEnum.NOT_FLAT_ACCOUNT.getType());
diffPO.setStatus(DataStatusEnum.NORMAL.getType());
diffPO.setCreateTime(new Date());
saveDiffShortList.add(SqlUtil.durableData(diffPO, PlatformConstants.TABLE_SAVE));
}
}
count++;
}
}
LOG.info("【微信对账业务】-短款对账完毕");
if (!saveDiffShortList.isEmpty()) { //将对账有出入的数据批量保存
iWechatPayDao.batchSave(saveDiffShortList);
}
//再以微信订单为主比对商户后台订单,如微信订单有,商户后台没有则有可能是因为银行日切日早于商户的所以造成的数据出入,故先在日期中的短款数据中匹配,如有则平账,否则保存为长款
//从服务器所存储账单的路径下读取账单信息并开始对账服务
billInfoArr = this.readGzipBillInfo(FileCatalogEnum.WECHAT_DOWNLOAD_BILL_FILE.getType()+dateFormat.format(ca.getTime())+".gzip");
for(String[] strArr : billInfoArr){
int count = 1;
Date tradeTime = dateFormatTime.parse(strArr[0]);
String appid = strArr[1];
String mchid = strArr[2];
String wechatOrderId = strArr[5];
String wechatTradeId = strArr[6];
String wechatAccount = strArr[12];
for(PaymentTradePO tradePO : paymentTradePOList){
String tradeId = tradePO.getTradeId();
String account = tradePO.getPrice().setScale(2, BigDecimal.ROUND_HALF_UP).toString();
if(wechatTradeId.equals(tradeId) && wechatAccount.equals(account)){
paymentTradePOList.remove(count-1);
break;
}
//出现金额不一致的情况
else if(tradeId.equals(wechatTradeId) && !account.equals(wechatAccount)){
WechatCheckAccountOrderDiffPO diffPO = new WechatCheckAccountOrderDiffPO(appid,mchid);
diffPO.setOrderDiffId(LogicIdUtil.bussinessId());
diffPO.setBussinessOrderNum(wechatTradeId);
diffPO.setWechatOrderNum(wechatOrderId);
diffPO.setOrderTime(tradePO.getCreateTime());
diffPO.setTradeSuccessTime(tradeTime);
diffPO.setWechatTradeAmount(new BigDecimal(wechatAccount));
diffPO.setBussinessTradeAmount(tradePO.getPrice());
diffPO.setDiffType(WechatCheckBilDiffTypeEnum.INCONSISTENT_AMOUNT.getType());
diffPO.setDiffOrderStatus(WechatCheckBilDiffStatusEnum.NOT_FLAT_ACCOUNT.getType());
diffPO.setStatus(DataStatusEnum.NORMAL.getType());
diffPO.setCreateTime(new Date());
if(iWechatPayDao.getInconsistentAmountByTradeId(wechatTradeId)==0){
saveDiffLongList.add(SqlUtil.durableData(diffPO, PlatformConstants.TABLE_SAVE));
}
break;
}
if(count==paymentTradePOList.size()){
//此时就出现长款
if(!wechatTradeId.equals(tradeId)){
//先排除因微信日切日早于商户平台的,故先在日期中的短款数据中匹配,如有则平账,否则保存为长款
Integer infoCount = iWechatPayDao.getIsCheckDiffBillByTradeId(wechatTradeId,wechatAccount);
if(infoCount>0){
iWechatPayDao.updateDiffOrderStatus(wechatTradeId);
}else{
WechatCheckAccountOrderDiffPO diffPO = new WechatCheckAccountOrderDiffPO(appid,mchid);
diffPO.setOrderDiffId(LogicIdUtil.bussinessId());
diffPO.setBussinessOrderNum(wechatTradeId);
diffPO.setWechatOrderNum(wechatOrderId);
diffPO.setTradeSuccessTime(tradeTime);
diffPO.setWechatTradeAmount(new BigDecimal(wechatAccount));
diffPO.setDiffType(WechatCheckBilDiffTypeEnum.LONG_SECTION.getType());
diffPO.setDiffOrderStatus(WechatCheckBilDiffStatusEnum.NOT_FLAT_ACCOUNT.getType());
diffPO.setStatus(DataStatusEnum.NORMAL.getType());
diffPO.setCreateTime(new Date());
saveDiffLongList.add(SqlUtil.durableData(diffPO, PlatformConstants.TABLE_SAVE));
}
}
}
count++;
}
}
LOG.info("【微信对账业务】-长款对账完毕");
if (!saveDiffLongList.isEmpty()) { //将对账有出入的数据批量保存
iWechatPayDao.batchSave(saveDiffLongList);
}
}
//对账逻辑处理完毕,将服务器中的文件删除
this.deleteServerFile(FileCatalogEnum.WECHAT_DOWNLOAD_BILL_FILE.getType()+dateFormat.format(ca.getTime())+".gzip");
}
}catch (Exception e){
e.printStackTrace();
}
});
}
自动对账中所涉及的工具类
生成签名的工具类(key为商户调置的密钥)
public static String getWechatSign(Map<Object,Object> map,String key){
ArrayList<String> list = new ArrayList<String>();
for(Map.Entry<Object,Object> entry:map.entrySet()){
if(entry.getValue()!=""){
list.add(entry.getKey() + "=" + entry.getValue() + "&");
}
}
int size = list.size();
String [] arrayToSort = list.toArray(new String[size]);
Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
StringBuilder sb = new StringBuilder();
for(int i = 0; i < size; i ++) {
sb.append(arrayToSort[i]);
}
String result = sb.toString();
result += "key=" + key;
LOG.error("sign Before MD5:" + result);
result = md5(result).toUpperCase();
LOG.error("sign Result:" + result);
return result;
}
将参数MAP转成XML
public static String converterPayPalm(Map<Object, Object> dataMap) {
StringBuilder strBuilder = new StringBuilder();
try {
strBuilder.append("<xml>");
Set<Object> objSet = dataMap.keySet();
for (Object key : objSet) {
if (key == null) {
continue;
}
//strBuilder.append("\n");
strBuilder.append("<").append(key.toString()).append(">");
Object value = dataMap.get(key);
strBuilder.append(coverter(value).trim());
strBuilder.append("</").append(key.toString()).append(">");
}
strBuilder.append("</xml>");
} catch (Exception e) {
LOG.error("MAP转换XML异常:" + e);
}
return strBuilder.toString();
}
下载微信对账单
urlPath:微信提供的下载URL
saveDir:下载后的保存路径
content:请求参数
public static Boolean downloadFile(String urlPath, String saveDir,String content) throws Exception {
URL url = new URL(urlPath);
// 连接类的父类,抽象类
URLConnection urlConnection = url.openConnection();
// http的连接类
HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;
// 设定请求的方法,默认是GET(对于知识库的附件服务器必须是GET,如果是POST会返回405。流程附件迁移功能里面必须是POST,有所区分。)
httpURLConnection.setRequestMethod("POST");
httpURLConnection.setDoOutput(true);
httpURLConnection.setRequestProperty("Accept", "application/json;");
httpURLConnection.setRequestProperty("Content-Type", "application/json;charset=utf-8");
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(httpURLConnection.getOutputStream(),
PlatformConstants.DEFAULT_CHARSET));
writer.write(content.toString());
writer.flush();
writer.close();
// 设置字符编码
// 打开到此 URL 引用的资源的通信链接(如果尚未建立这样的连接)。
int code = httpURLConnection.getResponseCode();
InputStream inputStream = httpURLConnection.getInputStream();
if(inputStream.available()!=149){
System.out.println("结果:"+inputStream.available());
File file = new File(saveDir);
if (!file.getParentFile().exists()) {
boolean result = file.getParentFile().mkdirs();
if (!result) {
System.out.println("创建失败");
}
}
OutputStream out = new FileOutputStream(file);
int size = 0;
int lent = 0;
byte[] buf = new byte[1024];
while ((size = inputStream.read(buf)) != -1) {
lent += size;
out.write(buf, 0, size);
}
inputStream.close();
out.close();
return true;
}
LOG.info("【微信对账业务】-T日无账单");
return false;
}
读取压缩文件中的账单信息的方法
参数path:路径+文件名
public List<String[]> readGzipBillInfo(String path){
Calendar ca = Calendar.getInstance();
ca.setTime(new Date());
ca.add(Calendar.DATE, -1);
SimpleDateFormat dateFormat= new SimpleDateFormat("yyyyMMdd");
SimpleDateFormat dateFormatStr= new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat dateFormatTime= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
File file=new File(path);
if(!file.exists()){
return null;
}
BufferedReader reader = new BufferedReader(
new InputStreamReader(
new GZIPInputStream(
new FileInputStream(path)), "utf-8"));
String line = null;
StringBuilder result = new StringBuilder();
while ((line = reader.readLine()) != null) {
result.append(line);
}
String tradeMsg = result.substring(result.indexOf("`"));
//首先比较微信对账中的总金额和商户后台订单总金额是否相等,如相等则不进行逐一比对
String totalAmount = iWechatPayDao.getTradeTotal(dateFormatStr.format(ca.getTime()));
String listStr = "";
String objStr = "";
if(tradeMsg.indexOf("总交易单数")>=0){
listStr = tradeMsg.substring(0, tradeMsg.indexOf("总交易单数"));
objStr = tradeMsg.substring(tradeMsg.indexOf("总交易单数"));
String[] i = objStr.replaceAll("`","").split("手续费总金额");
String[] acount = i[1].split(",");
if(acount[1].equals(totalAmount)){
return null;
}
}
String tradeInfo = tradeMsg.substring(0,tradeMsg.indexOf("总")).replace("`", "");// 去掉汇总数据,并且去掉'`'
//用spilt方法拿出每一天数据放进数组里。之后再用spilt方法把数据放进二维数组里。
String[] tradeArray = tradeInfo.split("%"); // 根据%来区分
List<String[]> tradeList = new ArrayList<>();
String[] tradeDetailArray = null;
for (String tradeDetailInfo : tradeArray) {
tradeDetailArray = tradeDetailInfo.split(",");
tradeList.add(tradeDetailArray);
}
String tradeTotalMsg =tradeMsg.substring(tradeMsg.indexOf("总"));
String tradeTotalInfo =tradeTotalMsg.substring(tradeTotalMsg.indexOf("`"));
return tradeList;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
向FTP服务器上传文件
/**
* Description: 向FTP服务器上传文件
* @param url FTP服务器hostname
* @param port FTP服务器端口
* @param username FTP登录账号
* @param password FTP登录密码
* @param path FTP服务器保存目录
* @param filename 上传到FTP服务器上的文件名
* @param input 输入流
* @return 成功返回true,否则返回false
*/
public static void uploadFile(String url, int port, String username, String password, String path, String filename, InputStream input) {
boolean success = false;
Map<String, Object> map = new HashMap<String,Object>();
String filepath="";
FTPClient ftp = new FTPClient();
try {
int reply;
ftp.connect(url, port);//连接FTP服务器
//如果采用默认端口,可以使用ftp.connect(url)的方式直接连接FTP服务器
ftp.login(username, password);//登录
reply = ftp.getReplyCode();
//判断reply的code是否正常,一般正常为2开头的code
if (!FTPReply.isPositiveCompletion(reply)) {
ftp.disconnect();
map.put("status", success);
map.put("filepath", filepath);
}
boolean a = ftp.changeWorkingDirectory("/");
System.out.println("转换目录结果1:"+a);
if(StringUtils.isBlank(path) || path.equals("/")){//如果输入的路径为空或者为根路径,则不转换操作目录
}else{//否则创建想要上传文件的目录,并且将操作目录转为新创建的目录
ftp.makeDirectory(path);
boolean b = ftp.changeWorkingDirectory(path);
System.out.println("转换目录结果2:"+b);
}
//ftp上传文件是以文本形式传输的,所以多媒体文件会失真,需要转为二进制形式传输
ftp.setFileType(FTP.BINARY_FILE_TYPE);
//转码后可以文件名可以为中文
ftp.storeFile(new String(filename.getBytes("GBK"), "iso-8859-1"), input);
input.close();
ftp.logout();
LOG.info("【微信对账业务】-账单成功上传至FTP服务器");
/* success = true;
filepath = "ftp://"+url+":"+port+"/"+path+"/"+filename;
map.put("status", success);
map.put("filepath", filepath);*/
} catch (Exception e) {
e.printStackTrace();
} finally {
if (ftp.isConnected()) {
try {
ftp.disconnect();
} catch (Exception ioe) {
}
}
}
}
删除服务器的文件
/**
* 删除服务上的文件
* @date 2018/08/29
* @param filePath 路径
* @return
*/
public static boolean deleteServerFile(String filePath){
boolean delete_flag = false;
File file = new File(filePath);
if (file.exists() && file.isFile() && file.delete())
delete_flag = true;
else
delete_flag = false;
return delete_flag;
}