OC 技术 苹果内购

一直觉得自己写的不是技术,而是情怀,一个个的教程是自己这一路走来的痕迹。靠专业技能的成功是最具可复制性的,希望我的这条路能让你们少走弯路,希望我能帮你们抹去知识的蒙尘,希望我能帮你们理清知识的脉络,希望未来技术之巅上有你们也有我。

OC 技术 苹果内购视频解说

苹果内购(视频讲解的封装)代码下载地址

前言

之前做过内购的需求,然后把整个过程记录下来,以防将来忘记之后回忆起来。

内购流程

在这里插入图片描述

内购代码逻辑

在这里插入图片描述

代码解析

下面的图片是整个苹果内购封装好的方法,可以直接拖过去用的,如果需要修改的就只有ValidationVoucherModel这个类,需要更改为对应公司的接口,里面的代码看多几篇就熟悉了。下面的图片里面,所有类的封装文件都是为IPAPurchase这个类服务的,再开发调用主要就是使用IPAPurchase就可以了
在这里插入图片描述
下面我会根据交易的整个流程解析代码的逻辑

启动监听

首先App启动的时候需要使用单例,初始化内购的监听方法,两个作用:1.每当app进行内购支付的时候,支付成功失败都会走代理的方法。2.app每次启动都回去调用该代理看看有没有没有走完内购的流程的订单。没有就会继续走一遍完成它

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
      //创建内购监听
      IPAPurchase.manager()?.startManager()
      
        return true
    }
}

结束监听

app回到后台就移除监听

func applicationDidEnterBackground(_ application: UIApplication) {
  IPAPurchase.manager()?.stopManager()
}

购买商品的方法

在商品购买的按键方法里面,通过单例直接调用购买的封装方法就可以了

IPAPurchase.manager()?.buyProduct(withProductID: "201912040101", currentBaseUrl: "http://192.168.1.20:8082/", currentAccountID: String(User.default.accountVO.id), currentOrderID: String(), currentVC: self, payResult: { (isSuccess, result, errorMsg) in
  print("result = \(String(describing: result))")
  if isSuccess {
    print("购买成功")
  }else{
    print("购买失败 - \(String(describing: errorMsg))")
  }
})

接下来下面的代码解析就是根据把支付的方法一个一个点进去深入解析内购的流程,解析整个代码逻辑,拆解代码详细解析

发起购买的内部方法

#pragma mark -- 发起购买的方法
-(void)buyProductWithProductID:(NSString *)productID CurrentBaseUrl:(NSString *)baseUrl CurrentAccountID:(NSString *)accountID currentOrderID:(NSString *)orderID currentVC:(UIViewController *)vc payResult:(PayResult)payResult{
  
  //保存产品ID
  self.productId = productID;
  //保存baseUrl
  self.baseUrl = baseUrl;
  //保存用户ID
  self.accountID = accountID;
  //保存订单ID
  self.orderID = orderID;
  //保存当前控制器
  self.vc = vc;
  
  //结束上次未完成的交易
  [self removeAllUncompleteTransactionsBeforeNewPurchase];
  //绑定闭包
  self.payResultBlock = payResult;
  
  //提示框购买中
  

  //如果产品ID为空
  if (!self.productId.length) {
    //显示产品ID为空
    UIAlertController*alet=[UIAlertController alertControllerWithTitle:nil message:@"没有相应的产品" preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction*sure=[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
    [alet addAction:sure];
    [vc presentViewController:alet animated:YES completion:nil];
    
  }
  //检测是否允许内购
  if ([SKPaymentQueue canMakePayments]) {
    //向苹果发起内购产品列表
    [self requestProductInfo:self.productId];
  }else{//请打开应用程序付费购买功能
    
    UIAlertController*alet=[UIAlertController alertControllerWithTitle:nil message:@"请打开应用程序付费购买功能" preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction*sure=[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
    [alet addAction:sure];
    [vc presentViewController:alet animated:YES completion:nil];
    
  }
  
}

检测未完成的订单

在购买之前,先看看有没有上一个没有完成的订单,如果有就结束上一次未完成的订单。

//结束上次未完成的交易
[self removeAllUncompleteTransactionsBeforeNewPurchase];

#pragma mark -- 结束上次未完成的交易
-(void)removeAllUncompleteTransactionsBeforeNewPurchase{
    //查看数组中是否有未完成的订单
    NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
    
    if (transactions.count >= 1) {
        for (NSInteger count = transactions.count; count > 0; count--) {
            SKPaymentTransaction* transaction = [transactions objectAtIndex:count-1];
            if (transaction.transactionState == SKPaymentTransactionStatePurchased||transaction.transactionState == SKPaymentTransactionStateRestored) {
                
                [[SKPaymentQueue defaultQueue]finishTransaction:transaction];
            }
        }
    }else{
         NSLog(@"没有历史未消耗订单");
    }
}

检测是否允许内购

结束上一次未完成的订单后,检测是否允许内购,向苹果发起获取内购产品列表

//检测是否允许内购
if ([SKPaymentQueue canMakePayments]) {
  //向苹果发起内购产品列表
  [self requestProductInfo:self.productId];
}else{//请打开应用程序付费购买功能
  UIAlertController*alet=[UIAlertController alertControllerWithTitle:nil message:@"请打开应用程序付费购买功能" preferredStyle:UIAlertControllerStyleAlert];
  UIAlertAction*sure=[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
  [alet addAction:sure];
  [vc presentViewController:alet animated:YES completion:nil];
}

发起请求

获取代码的列表发送请求的详细代码就是主要是多请求对象准守一下代理

#pragma mark -- 发起购买请求   检索产品  去苹果开发网站查看这个商品是否存在  是否存在通过代理SKProductsRequestDelegate  返回结果
-(void)requestProductInfo:(NSString *)productID{
    
    NSArray * productArray = [[NSArray alloc]initWithObjects:productID,nil];
    
    NSSet * IDSet = [NSSet setWithArray:productArray];
     //把产品ID封装成一个SKProductsRequest请求对象
    request = [[SKProductsRequest alloc] initWithProductIdentifiers:IDSet];
    //对响应c方法
    request.delegate = self;
    //发送商品请求
    [request start];
    
}

获取商品列表

发起请求之后,产品列表是通过代理的形式返回的,拿到商品id之后就发起支付

#pragma mark -- SKProductsRequestDelegate 查询成功后的回调
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
    //把所有的产品检索回来
    NSArray *myProduct = response.products;    
    if (myProduct.count == 0) {
      
        //提示框,没有商品
        if (self.payResultBlock) {
            self.payResultBlock(NO, nil, @"无法获取产品信息,购买失败");
        }
        return;
    }
    //用于保存产品对象(其中产品ID就在里面)
    SKProduct * product = nil;

	//打印所有列表的相关信息
    for(SKProduct * pro in myProduct){
        NSLog(@"SKProduct 描述信息%@", [pro description]);
        NSLog(@"产品标题 %@" , pro.localizedTitle);
        NSLog(@"产品描述信息: %@" , pro.localizedDescription);
        NSLog(@"价格: %@" , pro.price);
        NSLog(@"Product id: %@" , pro.productIdentifier);
        if ([pro.productIdentifier isEqualToString:self.productId]) {
            product = pro;
            break;
        }
    }
    
    //如果产品不为空
    if (product) {
        //把产品添加到支付对象里面
        SKMutablePayment * payment = [SKMutablePayment paymentWithProduct:product];
        //使用苹果提供的属性,将平台订单号复制给这个属性作为透传参数
        payment.applicationUsername = self.orderID;
        //保存用户ID  目的是防止漏单后,用户登录App登录了别的用户,然后App有漏单,一启动走补单流程,就充错人
        [[NSUserDefaults standardUserDefaults] setObject:self.accountID forKey:@"unlock_iap_userId"];
        //调用购买产品接口  购买成功之后会去到updatedTransactions回调
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }else{
        NSLog(@"没有此商品信息");
    }
}

支付结果代理回调

支付成功失败都会通过代理的形式在枚举中反应。

#pragma mark -- 监听结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
  //当用户购买的操作有结果时,就会触发下面的回调函数,
  for (SKPaymentTransaction * transaction in transactions) {
    //根据返回的状态进行处理
    switch (transaction.transactionState) {
        //交易完成
      case SKPaymentTransactionStatePurchased:{
        [self completeTransaction:transaction];
      }break;
        //交易失败
      case SKPaymentTransactionStateFailed:{
        [self failedTransaction:transaction];
      }break;
        //已经购买过该商品
      case SKPaymentTransactionStateRestored:{
        [self restoreTransaction:transaction];
      }break;
        //正在购买中...
      case SKPaymentTransactionStatePurchasing:{
        NSLog(@"正在购买中...");
        //[[SKPaymentQueue defaultQueue]finishTransaction:transaction];(会蹦)
      }break;
        //最终状态未确定
      case SKPaymentTransactionStateDeferred:{
        NSLog(@"最终状态未确定");
      }break;
      default:
        break;
    }
  }
}

然后调用成交交易的方法

#pragma mark -- 交易完成的回调
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
  //获取交易成功后的购买凭证
  [self getAndSaveReceipt:transaction];  
}

保存凭证,发送服务器验证

获取交易成功后的购买凭证
1.获取到凭证之后首先保存起来
2.使用后台提供的接口给后台去苹果服务器去进行验证

#pragma mark -- 获取购买凭证  并保存到Tmp文件里面去
-(void)getAndSaveReceipt:(SKPaymentTransaction *)transaction{
  //存储  向服务器验证凭证前的 凭证 订单号 用户ID 交易时间(简单:向服务器验证前保存一下)
  NSMutableDictionary *dict = [self saveServeCheskWithSaveReceipt:transaction];
  //向服务器发送订单ID 凭证验证  购买  然后发货,发货的动作其实就是服务器在订单的某一个属性里面修改状态。
  [self sendAppStoreRequestBuyWithReceipt:dict[@"receipt_key"] userId:dict[@"user_id"] paltFormOrder:dict[@"order"] trans:transaction];
}

下面的方法就是保存整个内购的过程

#pragma mark -- 存储  向服务器验证凭证前的 凭证 订单号 用户ID 交易时间(简单:向服务器验证前保存一下)
-(NSMutableDictionary *)saveServeCheskWithSaveReceipt:(SKPaymentTransaction *)transaction{
  
  //初始化字典
  NSMutableDictionary * dic = [[NSMutableDictionary alloc]init];
  //保存凭证到字典里面
  //获取交易凭证
  NSURL * receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
  NSData * receiptData = [NSData dataWithContentsOfURL:receiptUrl];
  NSString * base64String = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
  NSLog(@"base64String - %@",base64String);
  [dic setValue: base64String forKey:@"receipt_key"];
  //保存订单号到字典里面
  //透传参数  把刚刚购买前 transaction.payment.applicationUsername   里面存储的订单号那回来
  /*
   为什么不从上面的self.order获取你,如果这样获取就不会出现空的问题?
   这样想是错的。如果这样想的话,这个applicationUsername的参数就没有意义了。这个参数的设定是为了下面的坑而设定的。
   如果用户付款的之后,由于用户网络或者其他各种原因的问题,在没有拿到凭证之前(也就是没有进来updatedTransactions这个回调方法)把App退出。并且删了。这时候,用户重新下载App下.重新登录。由于订单没有结束(因为没有执行到)[[SKPaymentQueue defaultQueue]finishTransaction:transaction];这个方法。所以下次启动Appc注册监听之后,App马上会执行updatedTransactions的方法。方法的transaction对象会返回凭证给你,而applicationUsername属性里面存的就是你刚刚删除前的订单号。如果没有用applicationUsername设置对应凭证的订单号。就算给了凭证给你,你也不知道是哪张订单。到时候向公司服务器是验证不了的。
   */
  NSString *order = @"";
  if (transaction.payment.applicationUsername != nil) {
    order = transaction.payment.applicationUsername;
  }else{
    order = self.orderID;
  }
  //如果这个返回为nil
  NSLog(@"后台订单号 -- %@",order);
  [dic setValue: order forKey:@"order"];
  //保存交易时间到字典里面
  [dic setValue:[self getCurrentZoneTime] forKey:@"time"];
  NSString * userId;
  //NSUserDefaults 值得注意的是对相同的key赋值约等于一次覆盖
  
  /*我觉得这里有问题。我决得。保存userID应该放在[[SKPaymentQueue defaultQueue] addPayment:payment];前面保存userid,你放在这里的话要订单凭证成功返回才保存,如果用户付钱后,拿到凭证前删除的话就GG了,反正NSUserDefaults是覆盖的形式的。这个问题有待验证。因为你不存不成功,杀死APP后下次进来就没有UserID了。为了确保成功。应该在用户付钱前就要保存*/
  
//  //下面userid不为0的情况是用户成功交易,一直留在app才能走到这里,这时候就拿上保存用户ID。
//  if (self.accountID) {//如果用户ID不为空
//    userId = self.accountID;
//    //把用户ID存起来
//    [[NSUserDefaults standardUserDefaults] setObject:userId forKey:@"unlock_iap_userId"];
//  }else{//如果用户ID为空  用户进来userid为空的原因是,用户交易未完成的情况下,下次重新打开App上会执行updatedTransactions的方法。这时候userid未空的。为空的话就拿会App关掉前保存的userid
//    //从沙盒中获取用户ID
//    userId = [[NSUserDefaults standardUserDefaults] objectForKey:@"unlock_iap_userId"];
//  }
  //从沙盒中获取用户ID
  userId = [[NSUserDefaults standardUserDefaults] objectForKey:@"unlock_iap_userId"];
  //保存用户ID到字典里面
  [dic setValue: userId forKey:@"user_id"];
  
  //命名plist文件 生成唯一的字符串
  NSString *fileName = [NSString UUID];
  //沙盒Tmp 保存方式
  NSString *savedPath = [NSString stringWithFormat:@"%@/%@.plist", [SandBoxHelper iapReceiptPath], fileName];
  //这个存储成功与否其实无关紧要
  BOOL ifWriteSuccess = [dic writeToFile:savedPath atomically:YES];
  
  if (ifWriteSuccess){
    NSLog(@"购买凭据存储成功!");
    
  }else{
    NSLog(@"购买凭据存储失败");
  }
  
  return dic;
  
}

使用后台提供的接口给后台去苹果服务器去进行验证

#pragma mark -- 去服务器验证购买
-(void)sendAppStoreRequestBuyWithReceipt:(NSString *)receipt userId:(NSString *)userId paltFormOrder:(NSString * )order trans:(SKPaymentTransaction *)transaction{
    
    ValidationVoucherModel *model = [[ValidationVoucherModel alloc] init];
    [model getValidationVoucherRequestCurrentBaseUrl:self.baseUrl CurrentAccountID:userId currentOrderID:order currentReceipt:receipt DataSuccess:^(id _Nullable result) {
        NSLog(@"result - %@",result);
        BOOL changeOrder = result[@"data"][@"changeOrder"];
        if (changeOrder  == YES) {
            //删除  向服务器验证凭证后的 凭证 订单号 用户ID 交易时间(简单:验证成功后可以把凭证删除)
            [self delectedConsumptionOfGoodsWithReceipt:receipt];
            //存储  向服务器验证凭证前的 凭证 订单号 用户ID 交易时间(简单:向服务器验证前保存一下)
            [self saveServeCheskWithSaveReceipt:transaction];
            self.payResultBlock(YES, @"已经发货", @"");
            //结束订单
            [[SKPaymentQueue defaultQueue]finishTransaction:transaction];
        }else{
            NSLog(@"未发货");
        }
    } failedBlock:^(NSError * _Nonnull error) {
        NSLog(@"error - %@",error);
    }];
    
}

删除本地凭证

#pragma mark -- 删除  向服务器验证凭证后的 凭证 订单号 用户ID 交易时间(简单:验证成功后可以把凭证删除)
-(void)delectedConsumptionOfGoodsWithReceipt:(NSString * )receipt{
    
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError * error;
    if ([fileManager fileExistsAtPath:[SandBoxHelper iapReceiptPath]]) {
        NSArray * cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:[SandBoxHelper iapReceiptPath] error:&error];
        if (error == nil) {
            for (NSString * name in cacheFileNameArray) {
                NSString * filePath = [NSString stringWithFormat:@"%@/%@", [SandBoxHelper iapReceiptPath], name];
                NSFileManager *fileManager = [NSFileManager defaultManager];
                NSError * error;
                NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];
                NSString * localReceipt = [dic objectForKey:@"receipt_key"];
                //通过凭证进行对比
                if ([receipt isEqualToString:localReceipt]) {
                  BOOL ifRemove = [fileManager removeItemAtPath:filePath error:&error];
                  if (ifRemove) {
                    NSLog(@"验证凭证成功后 移除成功");
                  }else{
                    NSLog(@"验证凭证成功后 移除失败");
                  }
                }else{
                  NSLog(@"本地无与之匹配的订单");
                }
            }
        }
    }
}

结束交易(关键代码)

//结束订单
[[SKPaymentQueue defaultQueue]finishTransaction:transaction];

经验

1.关于支付丢单的问题

问题1:账号A: 订单A;支付成功了(拿到凭证),应该拿着(凭证,账号id,订单号)上存到后台验证,这个时候把App退出,重新启动,账号A登录,但是这个时候 订单A丢失了缓存没了,如何解决该丢单的问题。

解答:支付是可以把订单保存到payment.applicationUsername = self.orderID;的属性中,登录原来的账号,在未完成支付(即调用:[[SKPaymentQueue defaultQueue]finishTransaction:transaction];支付会仍然继续拿到刚支付的凭证)原来的账号,原来的订单id,原来的凭证就可以继续上存后台验证登录。
其实可以把订单id做一个本地保存,安全一定,防止payment.applicationUsername = self.orderID;获取回来的订单id为空。

问题2:账号A: 订单A;支付成功了(拿到凭证),应该拿着(凭证,账号id,订单号)上存到后台验证,这个时候把App删除,重新下载,账号A登录,但是这个时候 订单A丢失了(即使保存在本地,app删除也会把订单号删除),如何解决该丢单的问题

解答:支付是可以把订单保存到payment.applicationUsername = self.orderID;的属性中,登录原来的账号,在未完成支付(即调用:[[SKPaymentQueue defaultQueue]finishTransaction:transaction];支付会仍然继续拿到刚支付的凭证)原来的账号,原来的订单id,原来的凭证就可以继续上存后台验证登录。

问题3:账号A: 订单A;支付成功了(拿到凭证),应该拿着(凭证,账号id,订单号)上存到后台验证,这个时候把App退出,重新启动,账号B登录,但是这个时候刚刚账号A付款会变成账号B吗,上存后台服务器成功后

解答:会

问题3:账号A: 订单A;支付成功了(拿到凭证),应该拿着(凭证,账号id,订单号)上存到后台验证,这个时候把App退出,换另一台手机登录账号A,账号A还能够继续完成支付吗?

解答: 我没有似过,分两种情况:如果把之前的手机苹果id退出,在另一台手机登录苹果id,app登录账号A,如果能拿到凭证就能继续完成支付,如果手机苹果id登录拿不到凭证就无法继续完成支付。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/491586.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RHCE 补充:判断服务状态

内容补充&#xff1a;判断服务状态 systemctl 命令 系统控制管理命令工具 常用指令 1、启动 systemctl start 程序名 若要启动多个程序名&#xff0c;使用空格隔开&#xff0c;下同 2、重启&#xff1a;类似主机先断电再启动的一个状态 systemctl restart 程序名 3、停…

Spring Cloud 八:微服务架构中的数据管理

Spring Cloud 一&#xff1a;Spring Cloud 简介 Spring Cloud 二&#xff1a;核心组件解析 Spring Cloud 三&#xff1a;API网关深入探索与实战应用 Spring Cloud 四&#xff1a;微服务治理与安全 Spring Cloud 五&#xff1a;Spring Cloud与持续集成/持续部署&#xff08;CI/C…

图论最短路径以及floyd算法的MATLAB实现

图论是数学的一个分支&#xff0c;起源于18世纪。1736年&#xff0c;数学家欧拉通过解决“哥尼斯堡七桥问题”&#xff0c;将问题抽象成点和线的关系&#xff0c;并通过理论分析得出结论&#xff0c;这个过程标志着图论的产生&#xff0c;欧拉也因此被称为“图论之父”。图论研…

机器人机械手加装SycoTec 4060 ER-S电主轴高精密铣削加工

随着科技的不断发展&#xff0c;机器人技术正逐渐渗透到各个领域&#xff0c;展现出前所未有的潜力和应用价值。作为机器人技术的核心组成部分之一&#xff0c;机器人机械手以其高精度、高效率和高稳定性的优势&#xff0c;在机械加工、装配、检测等领域中发挥着举足轻重的作用…

利用RWKV-Runner初步感受一下ai的世界

最近又听到群里的高手在讨论RWKV-Runner&#xff0c;于是没忍住&#xff0c;就想试试&#xff0c;没想到第一关就卡住了。 从群里大咖上传的RWKV-Runner_windows_x64.exe文件开始吧&#xff0c;又找了个虚拟机&#xff0c;直接放在桌面上运行一下&#xff0c;结果就跳出一堆文…

Java框架安全篇--Shiro-550漏洞

Java框架安全篇--Shiro-550漏洞 Shiro反序列化源码可以提取&#xff1a; https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4 JAVA反序列化就不说了&#xff0c;可以参考前面文章 https://blog.csdn.net/m0_63138919/article/details/136751184 初始Apache Sh…

安科瑞智慧安全用电综合解决方案

概述 智慧用电管理云平台是智慧城市建设的延伸成果&#xff0c;将电力物联网技术与云平台的大数据分析功能相结合&#xff0c;实现用电信息的可视化管理&#xff0c;可帮助用户实现安全用电&#xff0c;节约用电&#xff0c;可靠用电。平台支持web&#xff0c;app&#xff0c;微…

智慧交通(代码实现案例)

1.项目简介 目标: 了解智慧交通项目的架构知道智慧交通项目中的模块能够完成智慧交通项目的环境搭建 该项目是智慧交通项目&#xff0c;通过该项目掌握计算机视觉的方法在交通领域的相关应用&#xff0c;包括车道线检测的方法&#xff0c;多目标车辆追踪及流量统计方法&#…

1.4.1 着色器

着色器&#xff08;Shader&#xff09;是运行在GPU上的小程序&#xff0c;这些小程序为图形渲染管线的某个特定部分而运行&#xff0c;从基本意义上来说&#xff0c;着色器只是一种把输入转化为输出的程序。 一、着色器类QOpenGLShaderProgram QOpenGLShaderProgram是Qt中对着…

thinkadmin 新版安装步骤

1.通过 Composer 安装: ( 推荐方式,默认只安装 admin 模块 ) ### 创建项目( 需要在英文目录下面执行 ) composer create-project zoujingli/thinkadmin### 进入项目根目录 cd thinkadmin### 数据库初始化并安装 ### 默认使用 Sqlite 数据库,若使用其他数据库请按第二步修…

I.MX6ULL_Linux_驱动篇(57)linux Regmap API驱动

我们在前面学习 I2C 和 SPI 驱动的时候&#xff0c;针对 I2C 和 SPI 设备寄存器的操作都是通过相关的 API 函数进行操作的。这样 Linux 内核中就会充斥着大量的重复、冗余代码&#xff0c;但是这些本质上都是对寄存器的操作&#xff0c;所以为了方便内核开发人员统一访问 I2C/S…

当当狸智能激光雕刻机 多种材质自由雕刻,轻松打造独一无二的作品

提及“激光雕刻”&#xff0c;大多数人的印象一般都是&#xff1a;笨重巨大、价格昂贵、操作复杂、使用门槛较高、调试难度大...不是普通人能够随意操作的&#xff0c;让人望尘莫及。 而小米有品上新的这台「当当狸桌面智能激光雕刻机L1」&#xff0c;将超乎你的想象&#xff…

ABAP - 上传文件模板到SMW0,并从SMW0上下载模板

upload file template to SMW0 and download the template from it 首先上传文件到tcode SMW0 选择新建后,输入文件名和描述,再选择想要上传的文件 上传完成后: 在表WWWPARAMS, WWWDATA里就会有信息存进去 然后就可以程序里写代码了: 屏幕上的效果:

界面控件DevExpress WinForms/WPF v23.2 - 电子表格支持表单控件

DevExpress WinForm拥有180组件和UI库&#xff0c;能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForm能完美构建流畅、美观且易于使用的应用程序&#xff0c;无论是Office风格的界面&#xff0c;还是分析处理大批量的业务数据&#xff0c;它都能轻松胜任…

yolov9报错:AttributeError: ‘list‘ object has no attribute ‘view‘的两种解决方法

1. 报错问题 In loss_tal.py: pred_distri, pred_scores torch.cat([xi.view(feats[0].shape[0], self.no, -1) for xi in feats], 2).split( (self.reg_max * 4, self.nc), 1) The error is as follows&#xff1a; AttributeError: list object has no attribute vie…

5.递归分治——1.递归与函数调用

程序运行 指令 指令的划分以函数为单位&#xff0c;调用函数本质上是使用call指令将pc指针移动到被调用的函数函数调用完成&#xff0c;需要使用return返回&#xff0c;本质是pc移回调用函数位置 数据 函数调用时&#xff0c;在堆栈区域要申请一片栈帧&#xff0c;函数的局…

linux用户管理命令2

useradd可以创建用户&#xff0c;其执行具体表现为在home文件夹下创建对应文件 那么有了useradd添加用户&#xff0c;自然有passwd添加用户密码 userdel可以删除用户&#xff0c;其中-r命令删除用户及其文件&#xff0c;-f命令可以强制删除用户&#xff0c;即使用户当前正在登录…

LeetCode 面试经典150题 205.同构字符串

题目&#xff1a; 给定两个字符串 s 和 t &#xff0c;判断它们是否是同构的。 如果 s 中的字符可以按某种映射关系替换得到 t &#xff0c;那么这两个字符串是同构的。 每个出现的字符都应当映射到另一个字符&#xff0c;同时不改变字符的顺序。不同字符不能映射到同一个字…

【MATLAB源码-第16期】基于matlab的MSK定是同步仿真,采用gardner算法和锁相环。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 **锁相环&#xff08;PLL&#xff09;** 是一种控制系统&#xff0c;用于将一个参考信号的相位与一个输入信号的相位同步。它在许多领域中都有应用&#xff0c;如通信、无线电、音频、视频和计算机系统。锁相环通常由以下几个…

什么是公网IP?

公网IP&#xff0c;即公开网络IP地址&#xff0c;是指在互联网中公开可见、可访问的IP地址。每个设备在连接互联网时&#xff0c;都需要一个唯一的公网IP地址&#xff0c;以便其他设备可以定位并与之通信。 尽管公网IP在网络通信中具有重要作用&#xff0c;但它也带来了一些安全…