业务实践系列(8):对账-YL行业支付前置平台交易账单数据解析

支付系统就一定需要对账,需要下载支付平台侧的账单与自己业务系统的交易数据时行对账。

YL行业支付前置平台交易账单:获取商户交易对账文件,根据请求头部serIdsvcId来获取服务商和商户的对应文件。

对账文件获取

对账文件获取:获取商户交易对账文件 ,根据请求头部 serIdsvcId 来获取服务商和商户的对应文件。

  1. 服务商下载服务商账单:serId必填,service(对账文件类型)填写pay.bill.serProvider;
  2. 服务商下载商户对账单:serId、svcId必填,service(对账文件类型)填写pay.bill.merchant;
  3. 商户下载商户对账单:svcId必填,service(对账文件类型)填写pay.bill.merchant。

交易账单

账单明细示例:

1
2
服务商,支付渠道类型,平台商户号,商户订单号,交易时间,公众账号ID,第三方商户号,商户号,子商户号,设备编号,外部平台订单号,第三方订单号,平台订单号,用户标识,交易类型,交易状态,付款银行,货币种类,总金额,企业红包金额,外部平台退款单号,平台退款单号,商户退款单号,退款金额,企业红包退款金额,退款类型,退款状态,商品名称,商户数据包,手续费,费率,终端类型,对账标识,门店编号,商户名称,收银员,子商户ID,免充值券金额,实收金额,结算金额,交易代码,代理机构标识码,发送机构标识码,系统跟踪号,商户类别,支付卡类型,原始交易日期,平台交易子类代码,平台交易子类,商户保留域,扩展字段2,扩展字段3,扩展字段4
`37000xxxxxxx,`Alipay,`A4729043xxxxxxxx,`51407xxxxx,`2020-03-04 16:58:34,`wxhnxxxxxxxxx,`111,`222,`6019597xxxxxxxxx,`POS_000,`2086073xxxxxxxxxxxxxxx9,`190783215xxxxxxxxxxxxx010,`7460119990439xxxxxxxxxxxxx92417,`HasxRDCxxxxxxxxxxxMZStonG,`pay.alipay,`支付成功,`GDBD,`CNY,`453.00,`0.00,`,`,`,`,`,`,`,`移动支付,`555,`1.58,`0.35%,`POS_000,`0,`szss-001,`铁路12306,`,`444,`5.00,`448.00,`446.42,`,`,`,`,`,`,`,`0301,`被扫消费,`resv,`,`,`

账单解析

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
/**
* @desc: 对账单
*/
@Data
@SuperBuilder
@Accessors(chain = true)
public class UnionPayBillDetail implements Serializable {
private static final long serialVersionUID = -5690435416282919149L;

/**
* 服务商ID
*/
private String serviceId;
/**
* 支付渠道类型(UNIONPAY \WEIXIN\ALIPAY)
*/
private String channelType;
/**
* 平台商户号
* (行业支付前置平台分配的商户号APPID)
*/
private String unionAppId;
/**
* 商户订单号(商户上送的订单号)
*/
private String mchOrderNo;
/**
* 交易时间
*/
private String tradeTime;
/**
* 公众号ID(微信公众账号appid)
*/
private String wxAppId;
/**
* 第三方商户号(如:微信商户号)
*/
private String thirdMchNo;
/**
* 商户号(条码支付平台分配的连锁商户号,如:755212000001)
*/
private String mchNO;
/**
* 子商户号(条码支付平台、全渠道分配的商户号,如:7551000001)
*/
private String subMchNo;
/**
* 设备号
*/
private String deviceNo;
/**
* 外部平台订单号
*/
private String outOrderNo;
/**
* 第三方订单号
*/
private String thirdOrderNo;
/**
* 平台订单号
*/
private String unionPayNo;
/**
* 用户标识
*/
private String userTag;
/**
* 交易类型
*/
private String tradeType;
/**
* 交易状态
*/
private String tradeStatus;
/**
* 付款银行
* (如:pay.weixin.native/pay.weixin.micropay/pay.weixin.jspayYL为00、01)
*/
private String bank;
/**
* 货币种类(CNY/GBP/HKD/USD/JPY/CAD/AUD/EUR)
*/
private String currencyType;
/**
* 总金额
*/
private String totalFee;
/**
* 企业红包
*/
private String redPackets;
/**
* 外部平台退款单号
*/
private String outRefundId;
/**
* 平台退款单号
*/
private String unionRefundNo;
/**
* 商户退款单号
*/
private String mchRefundNo;

/**
* 退款金额(单位:元)
*/
private String refundFee;
/**
* 企业红包退款金额
*/
private String redPacketsRefundFee;
/**
* 退款类型
*/
private String refundType;
/**
* 退款状态
*/
private String refundStatus;
/**
* 商品名称
* (原样返回提交支付时的body参数的值)
*/
private String goodsName;
/**
* 商户数据包
* (原样返回提交支付时的attach参数的值)
*/
private String mchData;
/**
* 手续费
*/
private String serviceFee;
/**
* 费率
*/
private String feeRate;
/**
* 终端类型
* (POS,ERP,SPAY_AND,SPAY_IOS,SPAY_POS,SPAY_PC)
*/
private String terminalType;
/**
* 对账标识
*/
private String verifyBillTag;
/**
* 门店编号
*/
private String doorNo;
/**
* 商户名称
*/
private String mchName;
/**
* 收银员
*/
private String cashier;

/**
* 子商户ID
*/
private String subAppId;
/**
* 免充值券金额
*/
private String couponFee;
/**
* 实收金额
*/
private String realFee;
/**
* 结算金额
*/
private String settlementFee;
/**
* 交易代码
*/
private String tradeCode;
/**
* 代理机构标识码
*/
private String proxyOrgCode;
/**
* 发送机构标识码
*/
private String sendOrgCode;
/**
* 系统跟踪号
*/
private String sysFollowCode;
/**
* 商户类别
*/
private String mchType;
/**
* 支付卡类型
*/
private String payCardType;
/**
* 原始交易日期
* yyyy-MM-ddHH:mm:ss
*/
private String originTradeTime;
/**
* 平台交易子类代码
*/
private String unionTradeSubTypeCode;
/**
* 平台交易子类
*/
private String unionTradeSubType;
/**
* 商户保留域
*/
private String mchExtendField;
/**
* 扩展字段2
*/
private String extendField2;
/**
* 扩展字段3
*/
private String extendField3;

/**
* 扩展字段4
*/
private String extendField4;
}

数据解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
/**
* @desc: 下载账单业务
*/
@Service
public class UnionPayBillServiceImpl implements UnionPayBillService {
private static final Logger logger = LogManager.getLogger(UnionPayBillServiceImpl.class);

private static final SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd");
private static final SimpleDateFormat sdf2 = new SimpleDateFormat("yyyyMMdd");

private static final String CHANNEL_CODE = "UNIONPAY";
private static final String TRADE_SCENE = "ACCEPT";
private static final String SUCCESS_CODE = "0000";

@Autowired(required = false)
private ChannelConfigFacade channelConfigFacade;
@Autowired
private PayProperties payProperties;

/**
* 账单数据封装
*
* @param downloadVO
* @return
*/
@Override
public HashMap<String, List<UnionPayBillDetail>> downloadUnionPayBill(DownloadVO downloadVO) {

ChannelConfigRequest channelConfigRequest = new ChannelConfigRequest();
channelConfigRequest.setOrgCode(downloadVO.getOrgCode());
channelConfigRequest.setMchAppId(downloadVO.getMchAppId());
channelConfigRequest.setChannelCode(CHANNEL_CODE);
channelConfigRequest.setTradeScene(TRADE_SCENE);
ChannelConfigResponse response = channelConfigFacade.query(channelConfigRequest);
List<ChannelConfigInfo> channelConfigList = response.getChannelConfigList();
if (CollectionUtils.isEmpty(channelConfigList)) {
throw new BusinessException("支付账号不存在");
}

String billDate = null;
try {
billDate = sdf2.format(sdf1.parse(downloadVO.getBillDate()));
} catch (ParseException e) {
throw new BusinessException("账单日期格式错误");
}

HashMap<String, List<UnionPayBillDetail>> billMap = new HashMap<>();
for (ChannelConfigInfo info : channelConfigList) {
// 构建请求账单接口参数
Map<String, String> paramsMap = buildRequestParams(info, billDate);
// 发送请求
String resultStr = Utils.sendPostGson(payProperties.getUnionPayGateway(), paramsMap);
logger.info("收到报文:{}", resultStr);
JSONObject jsonObject = JSONObject.parseObject(resultStr);
if (jsonObject.get("respCd").equals(SUCCESS_CODE)) {
String respContent = jsonObject.getString("respContent");
JSONObject respJson = JSONObject.parseObject(respContent);
String fileContent = (String) respJson.get("fileContent");
String base64 = fileContent.substring(fileContent.indexOf(",") + 1);
List<UnionPayBillDetail> billList = this.readBillData(base64);
billMap.put(info.getSvcId(), billList);

} else {
String respCd = (String) jsonObject.get("respCd");
String respMsg = (String) jsonObject.get("respMsg");
logger.warn("----->获取账单错误: respCd:{},respMsg:{}", respCd, respMsg);
}
}
return billMap;
}

/**
* 读取账单数据
*
* @param base64
* @return
*/
private List<UnionPayBillDetail> readBillData(String base64) {
if (StringUtils.isBlank(base64)) {
return Collections.EMPTY_LIST;
}

List<UnionPayBillDetail> billList = new ArrayList<>();
try {
byte[] bytes = Base64.decodeBase64(base64);
InputStream inputStream = new ByteArrayInputStream(bytes);
ZipInputStream zipIn = new ZipInputStream(inputStream, Charset.forName("GBK"));
// 不解压直接读取,加上GBK解决乱码问题
BufferedReader br = new BufferedReader(new InputStreamReader(zipIn, "GBK"));
ZipEntry zipFile;
// 循环读取zip中的cvs文件,无法使用jdk自带,因为文件名中有中文
while ((zipFile = zipIn.getNextEntry()) != null) {
if (zipFile.isDirectory()) {
// 目录不处理
}
// 获得cvs名字,检测文件是否存在
String fileName = zipFile.getName();
logger.info("----->解析账单文件:{}", fileName);
// 统一对账文件:UNIFIED
if (StringUtils.isNotBlank(fileName) && fileName.startsWith("UNIFIED") && fileName.contains(".")) {
String line;
int i = 0;
// 按行读取数据
while ((line = br.readLine()) != null) {
logger.info("----->解析数据行:{}", line);
// 数据从第2行开始读
if (i > 0 && line.startsWith("`")) {
String[] billArray = line.replace("`", "").split(",", -1);
UnionPayBillDetail unionPayBillDetail = UnionPayBillDetail.builder()
.serviceId(billArray[0])
.channelType(billArray[1])
.unionAppId(billArray[2])
.mchOrderNo(billArray[3])
.tradeTime(billArray[4])
.wxAppId(billArray[5])
.thirdMchNo(billArray[6])
.mchNO(billArray[7])
.subMchNo(billArray[8])
.deviceNo(billArray[9])
.outOrderNo(billArray[10])
.thirdOrderNo(billArray[11])
.unionPayNo(billArray[12])
.userTag(billArray[13])
.tradeType(billArray[14])
.tradeStatus(billArray[15])
.bank(billArray[16])
.currencyType(billArray[17])
.totalFee(billArray[18])
.redPackets(billArray[19])
.outRefundId(billArray[20])
.unionRefundNo(billArray[21])
.mchRefundNo(billArray[22])
.refundFee(billArray[23])
.redPacketsRefundFee(billArray[24])
.refundType(billArray[25])
.refundStatus(billArray[26])
.goodsName(billArray[27])
.mchData(billArray[28])
.serviceFee(billArray[29])
.feeRate(billArray[30])
.terminalType(billArray[31])
.verifyBillTag(billArray[32])
.doorNo(billArray[33])
.mchName(billArray[34])
.cashier(billArray[35])
.subAppId(billArray[36])
.couponFee(billArray[37])
.realFee(billArray[38])
.settlementFee(billArray[39])
.tradeCode(billArray[40])
.proxyOrgCode(billArray[41])
.sendOrgCode(billArray[42])
.sysFollowCode(billArray[43])
.mchType(billArray[44])
.payCardType(billArray[45])
.originTradeTime(billArray[46])
.unionTradeSubTypeCode(billArray[47])
.unionTradeSubType(billArray[48])
.mchExtendField(billArray[49])
.extendField2(billArray[50])
.extendField3(billArray[51])
.extendField4(billArray[52])
.build();
billList.add(unionPayBillDetail);
}
i++;
}
}
}
} catch (IOException e) {
logger.info("----->读取账单数据报错:{}", e.getMessage());
e.printStackTrace();
}
return billList;
}


/**
* 构建请求参数
*
* @param configInfo
* @param billDate
* @return
*/
private Map<String, String> buildRequestParams(ChannelConfigInfo configInfo, String billDate) {
//拼装参数Map
Map<String, String> paramsMap = new HashMap<String, String>();
paramsMap.put("svcId", configInfo.getSvcId());
// paramsMap.put("svcId", "A47290439980002");
paramsMap.put("svcApi", "up.fpsd.trade.utp.file");
paramsMap.put("format", "json");
paramsMap.put("charset", "utf-8");
paramsMap.put("signType", "RSA2");
paramsMap.put("timestamp", Utils.getTime());
//回调地址:自行修改
// paramsMap.put("notifyUrl", "http://180.169.111.158:8081/gateway/notify.do");
paramsMap.put("version", "2.0.0");
//业务字段
JSONObject bizContent = new JSONObject();
//文件类型
//01 对账文件
//02 报表文件
bizContent.put("fileType", "01");
//对账文件类型
//fileType 为 01 时填写以下
//01 YL交易对账文件
//02 AT 交易对账文件
//03 统一对账文件
bizContent.put("txnType", "03");
//交易时间 yyyyMMdd
bizContent.put("txnTime", billDate);
// bizContent.put("txnTime", "20200303");
//对账文件类型
// pay.bill.merchant 商 户 文件
//pay.bill.serProvider 服务
//商户对账文件
bizContent.put("service", "pay.bill.merchant");
//不填表示不压缩,
//0-不压缩
//1-压缩
//建议选择压缩方式
bizContent.put("compress", "1");

paramsMap.put("bizContent", bizContent.toJSONString());
String signature = null;
try {
signature = Utils.signparam(paramsMap, configInfo.getRsaPrivateKey());
// signature = Utils.signparam(paramsMap, Constants.HISPARAMRIKEY);
} catch (Exception ex) {
throw new BusinessException("签名错误");
}
paramsMap.put("sign", signature);
return paramsMap;
}

public static Boolean decryptByBase64(String base64, String filePath) {

if (StringUtils.isEmpty(base64) && StringUtils.isEmpty(filePath)) {
return Boolean.FALSE;
}
try {
int index = base64.indexOf(",");
String substring = base64.substring(base64.indexOf(",") + 1);
byte[] bytes = Base64.decodeBase64(base64.substring(base64.indexOf(",") + 1));
Files.write(Paths.get(filePath), bytes, StandardOpenOption.CREATE);
} catch (IOException e) {
e.printStackTrace();
}
return Boolean.TRUE;
}
}

业务实践系列(8):对账-YL行业支付前置平台交易账单数据解析

http://blog.gxitsky.com/2020/07/28/Business-08-union-pay-bill/

作者

光星

发布于

2020-07-28

更新于

2023-03-07

许可协议

评论