iOS小技能:封装银联接口协议(收银台的订单退款、查询机制)【下篇】

2022-08-22 11:04:37 浏览数 (1)

引言

  1. 上篇:业务功能、退款接口的协议规则、请求 https://kunnan.blog.csdn.net/article/details/115084885
  2. 下篇:返回结果处理、测试技巧、常见问题处理方案

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 状态判断逻辑

先判断协议字段返回,再判断业务返回,最后判断交易状态

1、返回状态码(status)参数:0表示调用成功;非0表示调用失败。此字段是通信标识,非交易标识,交易是否成功需要查看 result_code 来判断2、业务结果(result_code) : 0表示成功,非0表示失败注:此处返回0表示退款申请接收成功,实际的退款结果根据退款查询接口查询

代码语言:javascript复制
协议级错误返回:
<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对象
代码语言:javascript复制
 NSString * strResponse = [[NSString alloc] initWithData:response encoding:NSUTF8StringEncoding];
                CXMLDocument *xml= [[CXMLDocument alloc] initWithXMLString:strResponse options:0 error:nil];
                [weakSelf ProcessSucXML:xml];

  • 解析字段
代码语言:javascript复制
    // 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 订单列表数据追加本地数据:数据去重和按照时间戳排序

  • 数据去重

iOS数据搜索技巧:1、 应用NSPredicate进行数据筛选:从数组搜索特定条件的元素2、利用正则表达式进行匹配查找数据3、使用系统特定API进行数据查找来避免循环遍历数组

代码语言:javascript复制
/**
 数据去重:判断构造的数据,是否与接口返回的数据列表重复
 */
- (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;

}

  • 对象数组按照时间戳排序

iOS 排序指南:参数名ASCII码从小到大排序、数据按照日期进行分组、对象数组按照时间戳排序https://kunnan.blog.csdn.net/article/details/115242819

代码语言:javascript复制

/**
        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天的数据
代码语言:javascript复制
        // 数据过滤:  获取最近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—转入代发,退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者平台转账的方式进行退款。

  • 处理查询结果
代码语言:javascript复制
    
    [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 修改返回报文

  • 去掉空格之后,再使用。否则会导致节点的值会包含空格
  • 正确的格式
代码语言:javascript复制
<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

0 人点赞