引言
- 上篇:业务功能、退款接口的协议规则、请求 https://kunnan.blog.csdn.net/article/details/115084885
- 下篇:返回结果处理、测试技巧、常见问题处理方案
I 返回结果处理
1、申请退款成功,立马创建处理中的本地数据 2、退款查询,根据查询状态修改订单状态
- 数据按XML的格式实时返回
字段名 | 变量名 | 必填 | 类型 | 说明 |
---|---|---|---|---|
版本号 | version | 是 | String(8) | 版本号,version默认值是2.0。 |
字符集 | charset | 是 | String(8) | 可选值 UTF-8 ,默认为 UTF-8。 |
签名方式 | sign_type | 否 | String(8) | 签名类型,取值:MD5默认:MD5 |
授权交易机构 | sign_agentno | 否 | String(12) | 授权交易的服务商机构代码,商户授权给服务商交易的情况下返回,签名使用服务商的密钥 |
连锁商户号 | groupno | 否 | String(15) | 连锁商户为其下门店发交易的情况返回,签名使用连锁商户的密钥 |
返回状态码 | status | 是 | String(16) | 0表示成功,非0表示失败此字段是通信标识,非交易标识,交易是否成功需要查看 result_code 来判断 |
返回信息 | message | 否 | String(128) | 返回信息,如非空,为错误原因签名失败参数格式校验错误 |
网关返回码 | code | 否 | String(32) | 网关返回码 |
以下字段在 status 为 0的时候有返回 | ||||
业务结果 | result_code | 是 | String(16) | 0表示成功,非0表示失败注:此处返回0表示退款申请接收成功,实际的退款结果根据退款查询接口查询 |
商户号 | mch_id | 是 | String(15) | 商户号,由平台分配 |
设备号 | device_info | 否 | String(32) | 终端设备号 |
随机字符串 | nonce_str | 是 | String(32) | 随机字符串,不长于 32 位 |
错误代码 | err_code | 否 | String(32) | 具体错误码请看文档最后错误码列表 |
签名 | sign | 是 | String(32) | MD5签名结果,详见“安全规范” |
以下字段在 status 和 result_code 都为 0的时候有返回 | ||||
平台订单号 | transaction_id | 是 | String(32) | 平台交易号 |
商户订单号 | out_trade_no | 是 | String(32) | 商户系统内部的订单号 |
商户退款单号 | out_refund_no | 是 | String(32) | 商户退款单号 |
平台退款单号 | refund_id | 是 | String(32) | 平台退款单号 |
退款渠道 | refund_channel | 是 | String(16) | ORIGINAL—原路退款,默认 |
退款金额 | refund_fee | 是 | Int | 退款总金额,单位为分,可以做部分退款 |
现金券退款金额 | coupon_refund_fee | 否 | Int | 现金券退款金额 <= 退款金额, 退款金额-现金券退款金额为现金,单位为分 |
交易类型 | trade_type | 是 | String(32) | pay.weixin.micropay——微信被扫支付pay.alipay.micropay——支付宝被扫支付pay.unionpay.micropay――银联被扫支付 |
1.1 状态判断逻辑
先判断协议字段返回,再判断业务返回,最后判断交易状态
代码语言:javascript复制1、返回状态码(status)参数:0表示调用成功;非0表示调用失败。
此字段是通信标识,非交易标识,交易是否成功需要查看 result_code 来判断
2、业务结果(result_code) : 0表示成功,非0表示失败注:此处返回0表示退款申请接收成功,实际的退款结果根据退款查询接口查询
协议级错误返回:
<xml>
<status>500</status>
<message><![CDATA[SYSERR]]></message>
</xml>
正确返回数据:
<xml>
<status>0</status>
<message><![CDATA[OK]]></message>
<result_code>0</result_code>
</xml>
业务级错误返回:
<xml>
<status>0</status>
<message><![CDATA[OK]]></message>
<result_code>1</result_code>
<err_code><![CDATA[AUTHCODE_EXPIRE]]></err_code>
<err_code_des><![CDATA[二维码已过期,请刷新再试]]></err_code_des>
</xml>
1.2 字段解析(XML解析)
https://blog.csdn.net/z929118967/article/details/74747249
- 从Nsdata 转成CXMLDocument对象
NSString * strResponse = [[NSString alloc] initWithData:response encoding:NSUTF8StringEncoding];
CXMLDocument *xml= [[CXMLDocument alloc] initWithXMLString:strResponse options:0 error:nil];
[weakSelf ProcessSucXML:xml];
- 解析字段
// 1、字符串类型的解析:银行卡名字
[Session Instance].strBankNameReset = [[xml nodesForXPath:@"//ROOT/BODY/BNKNM" error:nil].lastObject stringValue];
// 2、integerValue类型的解析:订单金额
[Sessionsss sss].ssss = [[xml nodesForXPath:@"//ROOT/BODY/OsssMT" error:nil].lastObject stringValue].integerValue;
//3、数组的解析
NSArray *arr = [xml nodesForXPath:@"//ROOT/BODY/REC" error:nil];
//采用遍历CXMLElement方法
for (CXMLElement *element in arr)
{
NSString *strNumber = @"";
NSArray *aNumber = [element elementsForName:@"NO"];
if (aNumber != 0) {
strNumber = [aNumber.firstObject stringValue];
}
}
//————————————————
//版权声明:本文为CSDN博主「#公众号:iOS逆向」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
//原文链接:https://blog.csdn.net/z929118967/article/details/74747249
II、解决商户平台交易流水的订单记录无法实时与银联同步的问题
目前平台和银联的订单对账间隔是1天,因此需要在app本地创建退款失败、退款中、退款成功的订单数据。
2.1 订单列表数据追加本地数据:数据去重和按照时间戳排序
- 数据去重
代码语言:javascript复制iOS数据搜索技巧:1、 应用NSPredicate进行数据筛选:从数组搜索特定条件的元素2、利用正则表达式进行匹配查找数据3、使用系统特定API进行数据查找来避免循环遍历数组
/**
数据去重:判断构造的数据,是否与接口返回的数据列表重复
*/
- (BOOL )iscontainsinarr:(NSArray*)arr;
- (BOOL )iscontainsinarr:(NSArray*)arr{
NSPredicate* predicate = [NSPredicate predicateWithFormat:@"paymentNo == %@", self.paymentNo];
NSArray *arFiltered = [ arr filteredArrayUsingPredicate:predicate];//以一定的条件(特定日期)过滤数组,即进行大数据搜索。
if(arFiltered.count>0){
return YES;
}
return NO;
}
- 对象数组按照时间戳排序
代码语言:javascript复制iOS 排序指南:参数名ASCII码从小到大排序、数据按照日期进行分组、对象数组按照时间戳排序https://kunnan.blog.csdn.net/article/details/115242819
/**
weakSelf.viewModel.listModels= [QCT_Common sortedArrayUsingObjectKey:@"createTime" arr:weakSelf.viewModel.listModels];
IOS中将对象数组按照时间戳排序
*/
- (NSMutableArray*)sortedArrayUsingObjectKey:(NSString*)key arr:(NSArray*)listModels{
//1)取出日期分组
NSString* valueForKeyPath = FMSTR(@"@distinctUnionOfObjects.%@",key);
NSArray *arDistinct = [listModels valueForKeyPath:valueForKeyPath];
//listModels是一些含有日期属性的对象集合
//2)构建排序规则NSComparator
NSComparator cmptr = ^(id obj1, id obj2){
NSString *strData1 = obj1;
NSString *strData2 = obj2;
NSComparisonResult ret = [strData1 compare:strData2];
return ret;
};
// 3)按数字从小到大进行排序(将最新的数据显示在前面)
NSArray *arSorted = [arDistinct sortedArrayUsingComparator:cmptr];
arSorted = arSorted.reverseObjectEnumerator.allObjects;//顺序取反
NSMutableArray *newSorted_arr = [NSMutableArray array];
// 4)按照日期进行分组
for (NSString *strDateCreated in arSorted)
{
NSPredicate* predicate = [NSPredicate predicateWithFormat:@"createTime == %@", strDateCreated];
NSArray *arFiltered = [listModels filteredArrayUsingPredicate:predicate];//以一定的条件(特定日期)过滤maTemp数组,即进行大数据搜索。
[newSorted_arr addObjectsFromArray:arFiltered];
}
return newSorted_arr;
// ————————————————
// 版权声明:本文为CSDN博主「#公众号:iOS逆向」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
// 原文链接:https://blog.csdn.net/z929118967/article/details/115242819
// ————————————————
// 版权声明:本文为CSDN博主「#公众号:iOS逆向」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
// 原文链接:https://blog.csdn.net/z929118967/article/details/113499172
}
- 数据过滤: 获取最近7天的数据
// 数据过滤: 获取最近7天的数据
NSMutableString *str = [[NSMutableString alloc]initWithString: [QCT_Common get4TodayTimeWithDateFormat:@"yyyy-MM-dd HH:mm:ss"]];
NSMutableArray *tmp = [NSMutableArray array];
for (QCTReceiptDetailModel *obj in tmparr) {
NSInteger day = [QCT_Common contrastTimeBackDayWithStartDate:obj.completeTime endDate:str DateFormat:@"yyyy-MM-dd HH:mm:ss"];// endDate 大
NSLog(@"day: %ld",(long)day);
if (day > 6) {
}else{
[tmp addObject:obj];
}
}
/**
计算两个时间的间隔(天)
@param start 开始时间
@param end 结束时间
@return 间隔时间
*/
(NSInteger)contrastTimeBackDayWithStartDate:(NSString *)start endDate:(NSString *)end DateFormat:(NSString*)Format
{
NSTimeInterval time = [self contrastTimeWithStartDate:start endDate:end dateFormatter:Format];
NSInteger minute,second,hour,day;
second=(NSInteger)time00;
minute = (NSInteger)(time/60)`;
hour = (NSInteger)(time/3600)$;
day = (time/3600/24);
return day;
}
/**
计算两个时间的间隔(毫秒)
@param start 开始时间
@param end 结束时间
@return 间隔时间
*/
(NSTimeInterval)contrastTimeWithStartDate:(NSString *)start endDate:(NSString *)end dateFormatter:(NSString*)Formatter
{
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
// [dateFormatter setDateFormat:@"yyyy-MM-dd"];
[dateFormatter setDateFormat:Formatter];
NSDate *startDate = [dateFormatter dateFromString:start];
NSDate *endDate = [dateFormatter dateFromString:end];
NSTimeInterval time = [endDate timeIntervalSinceDate:startDate];
return time;
}
2.2 退款状态查询处理
https://up.95516.com/open/openapi/doc?index_1=2&index_2=1&chapter_1=274&chapter_2=296
- 退款状态
SUCCESS—退款成功 FAIL—退款失败 PROCESSING—退款处理中 NOTSURE—未确定, 需要商户原退款单号重新发起 CHANGE—转入代发,退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者平台转账的方式进行退款。
- 处理查询结果
[NetworkHelper4XML postWithURL:url params:params successBlock:^(CXMLDocument * _Nonnull xml) {
//$n 表示记录的序号,取值为 0~($ refund_count -1),例如 refund_count 指示返回的退款记录有 2 条。第一条序号为“0”,第二条序号为“1”。
// 退款笔数refund_count Int 退款记录数
NSString *refund_count = [[xml nodesForXPath:@"//xml/refund_count" error:nil].lastObject stringValue];
refund_count = [NSNumber numberWithInteger:(refund_count.integerValue-1)].description;
NSString* refund_status = [[xml nodesForXPath:FMSTR(@"%@%@",@"//xml/refund_status_",refund_count) error:nil].lastObject stringValue];
if([refund_status isEqualToString:@"SUCCESS"]){
// refund_time_$n 退款时间 yyyyMMddHHmmss
NSString* refund_time_ = [[xml nodesForXPath:FMSTR(@"%@%@",@"//xml/refund_time_",refund_count) error:nil].lastObject stringValue];
//商户退款单号
NSString* out_refund_no_ = [[xml nodesForXPath:FMSTR(@"%@%@",@"//xml/out_refund_no_",refund_count) error:nil].lastObject stringValue];
// 退款金额,单位为分,可以做部分退款
NSString* refund_fee_ =[[xml nodesForXPath:FMSTR(@"%@%@",@"//xml/refund_fee_",refund_count) error:nil].lastObject stringValue];
#pragma mark - ********退款成功界面: 退款编号、退款时间、退款人、退款金额、打印小票
payinfomodel.orderNo = out_refund_no_;
payinfomodel.paymentNo = out_refund_no_;
payinfomodel.createTime = [QCT_Common strdatedateFormat:@"yyyy-MM-dd HH:mm:ss" fromDateFormat:@"yyyyMMddHHmmss" objstr:refund_time_];// 格式化时间
payinfomodel.completeTime = payinfomodel.createTime;
model4list.completeTime =payinfomodel.createTime;
model4list.createTime =payinfomodel.createTime;
payinfomodel.paymentAmount = [NSNumber numberWithDouble: refund_fee_.doubleValue/100.00].description;
payinfomodel.stateName = @"退款成功";
payinfomodel.state = [NSNumber numberWithInt:k_PAY_STATE_Enum4PAY_STATE_SUCCESS].description;
model4list.state = payinfomodel.state;
model4list.stateName =payinfomodel.stateName;
[model4list bg_updatetoDB];
[[NSNotificationCenter defaultCenter] postNotificationName:QCTupdateInffo4ReceiptListNotification object: [NSNumber numberWithInteger:1] userInfo:@{QCTchangeViewNotificationuserInfo:NSClassFromString(@"QCTNewslist4ReceiptViewControllerNewViewController")}];// 只要门店需要筛选
QCTRefundSUCCModel *model4SUCCModel = payinfomodel.RefundSUCCModel;
QCTrefund_successfulViewController *vc = [QCTrefund_successfulViewController new];
vc.viewModel.model = model4SUCCModel;
[weakSelf.navigationController pushViewController:vc animated:YES];
NSLog(@"跳到成功页");
}else if([refund_status isEqualToString:@"CHANGE"]){
payinfomodel.stateName = @"其他错误";
payinfomodel.state = [NSNumber numberWithInt:k_PAY_STATE_Enum4PAY_STATE_PAYMENT_FAILURE].description;
payinfomodel.remark = @"转入代发,退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者平台转账的方式进行退款";
model4list.state = payinfomodel.state;
model4list.stateName =payinfomodel.stateName;
[LBAlertController showAlertTitle:nil content:@"CHANGE:转入代发" cancelString:@"确定" cancleBlock:^{
[self pop];
} sureString:nil sureBlock:nil
currentController:[QCT_Common getCurrentVC]];
}else if([refund_status isEqualToString:@"FAIL"]){//退款失败
payinfomodel.stateName = @"退款失败";
payinfomodel.state = [NSNumber numberWithInt:k_PAY_STATE_Enum4PAY_STATE_PAYMENT_FAILURE].description;
model4list.state = payinfomodel.state;
model4list.stateName =payinfomodel.stateName;
[LBAlertController showAlertTitle:nil content:@"退款失败" cancelString:@"确定" cancleBlock:^{
[self pop];
} sureString:nil sureBlock:nil
currentController:[QCT_Common getCurrentVC]];
}else if([refund_status isEqualToString:@"PROCESSING"]){//退款处理中
[self.view showHUDMessage:@"PROCESSING:退款处理中" ];
NSString* out_refund_no_ = [[xml nodesForXPath:FMSTR(@"%@%@",@"//xml/out_refund_no_",refund_count) error:nil].lastObject stringValue];
// 退款金额,单位为分,可以做部分退款
NSString* refund_fee_ =[[xml nodesForXPath:FMSTR(@"%@%@",@"//xml/refund_fee_",refund_count) error:nil].lastObject stringValue];
payinfomodel.paymentAmount = [NSNumber numberWithDouble: refund_fee_.doubleValue/100.00].description;
// NSLog(@"跳到退款中界面");
payinfomodel.orderNo = out_refund_no_;
payinfomodel.paymentNo = out_refund_no_;
model4list.paymentAmount = payinfomodel.paymentAmount ;
payinfomodel.createTime = weakSelf.viewModel.payinfomodel.createTime4out_refund_no;
// [QCT_Common strdate4YYMMDDWithyyyyMMddHHmmssstr:refund_time_];// 格式化
payinfomodel.completeTime = payinfomodel.createTime;
model4list.completeTime =payinfomodel.createTime;
model4list.createTime =payinfomodel.createTime;
QCTRefundSUCCModel *model = payinfomodel.RefundSUCCModel;
model.isFromFretless = YES;
QCTrefund_ingViewController *vc = [QCTrefund_ingViewController new];
vc.viewModel.model = model;
[weakSelf.navigationController pushViewController:vc animated:YES];
}else if([refund_status isEqualToString:@"NOTSURE"]){//退款处理中
// 新增备注信息: 转入代发,退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者平台转账的方式进行退款
payinfomodel.stateName = @"其他错误";
payinfomodel.state = [NSNumber numberWithInt:k_PAY_STATE_Enum4PAY_STATE_PAYMENT_FAILURE].description;
model4list.state = payinfomodel.state;
model4list.stateName =payinfomodel.stateName;
payinfomodel.remark =@"未确定,请重新发起退款"; // 新增备注信息:未确定,请重新发起退款
[LBAlertController showAlertTitle:nil content:@"NOTSURE:未确定,请重新发起退款" cancelString:@"确定" cancleBlock:^{
[self pop];
} sureString:nil sureBlock:nil
currentController:[QCT_Common getCurrentVC]];
}
#pragma mark - ******** 更新账单流水列表数据
[model4list bg_updatetoDB];
[[NSNotificationCenter defaultCenter] postNotificationName:QCTupdateInffo4ReceiptListNotification object: [NSNumber numberWithInteger:1] userInfo:@{QCTchangeViewNotificationuserInfo:NSClassFromString(@"QCTNewslist4ReceiptViewControllerNewViewController")}];// 只要门店需要筛选
} RspCDFailed:nil failure:nil isShowLoadingDataGif:YES];
IIII 测试技巧
3.1 产生订单数据
- 由于商户平台侧的订单数据和银联的是一天同步一次,因此可以提前一天使用支付宝多交易产生订单,以便第二天测试退款。
当天的退款金额<=收款金额
- iOS签名demo下载地址:从CSDN下载demo地址:https://download.csdn.net/download/u011018979/15483107
原理文章:https://blog.csdn.net/z929118967/article/details/108195721
- 在线测试签名地址:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=20_1
3.2 修改返回报文
- 去掉空格之后,再使用。否则会导致节点的值会包含空格
- 正确的格式
<xml><appid><![CDATA[]]></appid> <charset><![CDATA[UTF-8]]></charset> <coupon_refund_fee_0><![CDATA[0]]></coupon_refund_fee_0> <mch_id><![CDATA[]]></mch_id> <mdiscount_0><![CDATA[0]]></mdiscount_0> <nonce_str><![CDATA[]]></nonce_str> <out_refund_no_0><![CDATA[]]></out_refund_no_0> <out_trade_no><![CDATA[]]></out_trade_no> <out_transaction_id><![CDATA[]]></out_transaction_id> <refund_channel_0><![CDATA[ORIGINAL]]></refund_channel_0> <refund_count><![CDATA[1]]></refund_count> <refund_fee_0><![CDATA[1]]></refund_fee_0> <refund_id_0><![CDATA[]]></refund_id_0> <refund_status_0><![CDATA[SUCCESS]]></refund_status_0> <refund_time_0><![CDATA[]]></refund_time_0> <result_code><![CDATA[0]]></result_code> <sign><![CDATA[]]></sign> <sign_agentno><![CDATA[]]></sign_agentno> <sign_type><![CDATA[MD5]]></sign_type> <status><![CDATA[0]]></status> <total_fee><![CDATA[1]]></total_fee> <trade_type><![CDATA[pay.alipay.micropay]]></trade_type> <transaction_id><![CDATA[]]></transaction_id> <version><![CDATA[2.0]]></version>
</xml>
see also
openapi
java demo
https://github.com/zhangkn/web-pay-unionPay中国银联云闪付合作伙伴开放平台https://partner.95516.com/portal/document#pf29
iOS网络请求指南: 请求参数的拼接(签名)、返回参数解析(JSON/XML解析)
https://kunnan.blog.csdn.net/article/details/11517570