一.前言

很多游戏需要接入内购IAP,对于苹果端,我们直接对接苹果就行了,但是android平台太多,国内,我们需要接入支付宝,微信,或者华为支付,小米支付等。国外,我们需要接入谷歌支付,亚马逊等等,相对来说都是比较麻烦的,所以,一般我们使用聚合的支付SDK,会省很多力气。

二.什么是UnityIAP

Unity IAP 是Unity官方出的一个支付插件,可让我们轻松地在Unity中接入内购
Unity IAP 支持的商店如下所示:

商店名称 系统平台 版本 网站
Google支付 Android 3.0.3 Google ReleaseNotes
亚马逊应用商店 Android 2.0.76 Amazon SDK
Unity分发平台 Android 2.0.0 and higher UDP
IOS应用商店 MacOS/iOS/tvOS Store Kit v1 Apple Store Kit
微软应用商店 Windows Microsoft SDK

根据上表,我们可以知道,UnityIAP主要还是支持海外的应用内购,对国内的众多手机品牌内购暂不支持。
不过对于我们发海外的游戏来说,已经足够了。
下面我们来以Apple和Goolgle为例,说一下IAP的接入

三.导入SDK

有的教程包括官方的一些老教程,会引导我们打开Services面板,打开In-AppPurchasing 的开关,unity会自动导入IAP的插件,但是这个流程卡顿不说,团队协作时,每个人都需要打开Services的开关,否则会报错。
其实从Unity2019+的版本开始,我们就不需要从Services这里导入sdk了,直接走PackageManager就行。打开Unity的工具包管理PackageManager,搜索找到In App Purchasing插件,并导入到工程

四.定义产品

产品编号设置

输入跨平台唯一标识符,作为产品与应用商店通信时的默认 ID。
重要提示:ID 只能包含小写字母、数字、下划线或句点。

产品类别设置

每个产品必须是以下类型之一:

类型 描述 例子
消耗品 用户可以重复购买产品。消耗品无法恢复。 虚拟货币
健康药水
临时加电。
非消耗品 用户只能购买一次产品。非消耗品可以恢复。 武器或盔甲
访问额外内容
无广告
订阅 用户可以在有限的时间内访问产品。订阅产品可以恢复。 每月访问在线游戏
VIP 身份授予每日奖金
免费试用

产品元数据设置

本部分定义了与您的产品相关联的元数据,以便在游戏内商店中使用。
说明:使用以下字段为您的产品添加描述性文本:

场地 数据类型 描述 例子
产品区域设置 枚举 确定您所在地区可用的应用商店。 英语(美国)(Google Play、Apple)
产品名称 string 您的产品在应用商店中显示的名称。 “健康药水”
产品描述 string 您的产品在应用商店中出现的描述性文本,通常是对产品是什么的解释。 “恢复 50 点生命值。”

支出设置

支出设置是我们展示给购买者的内容,通过使用名称和数量标记产品,我们可以在购买时快速调整某些项目类型(例如,硬币或宝石)的游戏内数量。

场地 数据类型 描述 例子
支付类型 枚举 定义购买者收到的内容类别。有四种可能的类型。 货币
项目
资源
其他
支付子类型 string 为内容类别提供粒度级别。 货币类型的“金”和“银”子类型
物品类型的“药水”和“助推”子类型
数量 int 指定购买者在付款中收到的项目数、货币等。 1
>25
100
数据 以任何您喜欢的方式使用此字段作为在代码中引用的属性。 UI 元素的标志
物品稀有度

五.接入UnityIAP

1.初始化

新建一个类IAPManager,必须继承IStoreListener接口,UnityIAP内购事件通过此接口来通知我们。
调用UnityPurchasing.Initialize方法初始化IAP,我们需要传入相应的配置和商品信息进入。

注意:如果网络不可用,初始化不会失败;Unity IAP 将继续尝试在后台初始化。仅当 Unity IAP 遇到不可恢复的问题(例如配置错误或在设备设置中禁用 IAP)时,初始化才会失败。
因此 Unity IAP 可能需要任意时间来初始化;如果用户处于飞行模式,则无限期。如果初始化未成功完成,您应该通过防止用户尝试购买来相应地设计您的商店。

示例代码如下:

using UnityEngine;
using UnityEngine.Purchasing;public class MyIAPManager : IStoreListener {private IStoreController controller;private IExtensionProvider extensions;public MyIAPManager () {var module = StandardPurchasingModule.Instance();var builder = ConfigurationBuilder.Instance(module);builder.AddProduct("100_gold_coins", ProductType.Consumable, new IDs{{"100_gold_coins_google", GooglePlay.Name},{"100_gold_coins_mac", MacAppStore.Name}});UnityPurchasing.Initialize (this, builder);}///初始化成功的回调public void OnInitialized (IStoreController controller, IExtensionProvider extensions){this.controller = controller;this.extensions = extensions;}/// 初始化失败回调/// 当 Unity IAP 遇到不可恢复的初始化错误时调用。/// 请注意,如果网络不可用,不会调用此方法;unityIap将后台重试,直到可用为止。public void OnInitializeFailed (InitializationFailureReason error){}/// 购买完成回调/// 初始化成功后,随时可能会调用public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e){return PurchaseProcessingResult.Complete;}/// 购买失败public void OnPurchaseFailed (Product i, PurchaseFailureReason p){}
}

2.发起支付

当用户想要购买产品时,调用IStoreController.InitiatePurchase方法

// 当用户点击购买按钮,进入支付流程
public void OnPurchaseClicked(string productId) {controller.InitiatePurchase(productId);
}

发起支付后,无论是调用ProcessPurchase成功购买,还是OnPurchaseFailed失败。都将被异步通知结果.

3.支付回调

购买完成时会调用商店监听器的 ProcessPurchase() 函数。并且函数需要我们返回一个结果,来告诉IAP程序是否已完成对购买的处理,

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
{//商品定义信息,包括id,类型等public ProductDefinition definition { get; private set; }//商品元数据,包括价格,描述等public ProductMetadata metadata { get; internal set; }//唯一的交易id,作为一次购买的唯一idpublic string transactionID { get; internal set; }//交易订单的收据,很长很长的一个Base64加密的字符串,用来验证订单合法性public string receipt { get; internal set; }
}

函数返回值

结果 描述
PurchaseProcessingResult.Complete 应用程序已完成对购买的处理,不应再次向应用程序通知此事。
PurchaseProcessingResult.Pending 应用程序仍在处理购买,除非调用 IStoreController 的 ConfirmPendingPurchase 函数,否则将在下一次应用程序启动时再次调用 ProcessPurchase。

请注意,如果应用程序在 ProcessPurchase 处理程序执行过程中崩溃,那么在 Unity IAP 下次初始化时会再次调用它,因此我们需要重复数据删除功能。另外在初始化成功后,随时可能调用 ProcessPurchase。

Unity IAP 要求返回确认购买,以确保在网络中断或应用程序崩溃的情况下可靠地完成购买。在应用程序离线时完成的任何购买都将在下次初始化时发送给应用程序。

六.内购二次验证

1.立即完成购买

返回 PurchaseProcessingResult.Complete 时,Unity IAP 立即完成交易(如下图所示)。
如果我们的游戏需要服务器验证订单,并分发奖励(例如,在网络游戏中提供游戏币),那么我们就不能返回 PurchaseProcessingResult.Complete。

否则,如果在保存到云端之前卸载应用程序,则购买的消耗品将面临丢失的风险。

2.将购买保存到云端

如果要将消耗品购买交易保存到云端,我们必须返回 PurchaseProcessingResult.Pending,并且仅在成功的二次验证订单成功后,才返回 ConfirmPendingPurchase。

返回 Pending 时,Unity IAP 会在底层商店中保持交易为未结 (open) 状态,直至确认为已处理为止,因此确保了即使在消耗品处于此待处理状态时用户重新安装您的应用程序,消耗品购买交易也不会丢失。

3.收据验证

在函数PurchaseProcessing中返回Pending状态后,我们需要想苹果/谷歌的商店后台发送订单收据进行二次验证,即:VerifyReceipt

订单二次验证有两种方式

  • 1.客户端直接发送receipt收据到苹果后台,如果成功,直接发放商品
  • 2.客户端发送receipt到server,由server发送receipt收据到苹果后台,成功后返回客户端并发放商品
    按照安全性原则,客户端的所有信息都是不可信的,而且支付业务是游戏的核心模块,所以最好选择第二种方式。

ios的收据验证流程

验证服务器地址

  • 1.沙盒测试服务器地址(https://sandbox.itunes.apple.com/verifyReceipt)
  • 2.正式服务器地址(https://buy.itunes.apple.com/verifyReceipt)

客户端拿到receipt,并发送给server,server拿到receipt后,先向苹果正式服务器验证,如果苹果返回state 21007.则代表是沙盒测试环境,然后再向测试服务器进行验证。

4.常见攻击手段

说到支付安全,有些人对此不以为然,下面我给大家罗列一下常用的支付攻击手段

  • 1、劫持apple server攻击 => 通过dns污染,让客户端支付走到假的apple_server,并返回验证成功的response。 这个主要针对支付方式一 如果是支付方式二 就无效。
  • 2、重复验证攻击 => 一个receipt重复使用多次
  • 3、跨app攻击 => 别的app的receipt用到我们app中来
  • 4、换价格攻击 => 低价商品代替高价商品
  • 5、中间人攻击 => 伪造apple_server,如果用户支付就将

5.恢复交易

恢复交易只用于非消耗品或可续订的订阅商品,如果用户卸载后重新安装了应用程序时,我们应该给用户恢复这些商品,应用商店给每个用户提供了一条可供UnityIAP检索的永久记录,

在支持交易恢复功能的平台上(例如 Google Play 和通用 Windows 应用程序),Unity IAP 会在重新安装后的第一次初始化期间自动恢复用户拥有的任何商品;系统将为每项拥有的商品调用 IStoreListener 的 ProcessPurchase 方法。

在 Apple 平台上,用户必须输入密码才能检索以前的交易,因此您的应用程序必须为用户提供一个按钮来输入密码。此过程中将会针对用户已拥有的任何商品调用 IStoreListener 的 ProcessPurchase 方法。

/// <summary>
/// IStoreListener 对 OnInitialized 的实现。
/// </summary>
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{extensions.GetExtension<IAppleExtensions>().RestoreTransactions (result => {if (result) {// 这并不意味着已恢复任何对象,// 只表示恢复过程成功了。} else {//恢复操作已失败。}});
}

七.源代码

最后附上我的完整代码

using System;
using System.Collections.Generic;
using System.Text;
using Common;
using LitJson;
using UnityEngine;
using UnityEngine.Purchasing;
using XLua;namespace IAP
{// 从 IStoreListener 派生 Purchaser 类使其能够接收来自 Unity Purchasing 的消息。public class PurchaseManager : MonoSingleton<PurchaseManager>, IStoreListener{#if UNITY_IOSprivate string VerifyURL = URLSetting.BASE_URL + "/charge";
#elif UNITY_ANDROIDprivate string VerifyURL = URLSetting.BASE_URL + "/gp_charge";
#elseprivate string VerifyURL = URLSetting.BASE_URL + "/charge";
#endif[CSharpCallLua] public static event Action<Product[]> OnInitializedEvent;[CSharpCallLua] public static event Action<int> OnInitializeFailedEvent;[CSharpCallLua] public static event Action<ProductData> OnPurchaseSuccessEvent;[CSharpCallLua] public static event Action<int, ProductData> OnPurchaseFailedEvent;private IStoreController m_StoreController;         // Unity 采购系统private IExtensionProvider m_StoreExtensionProvider; // 商店特定的采购子系统.private string purchasingProductId;                //正在支付支付中的productIdprivate const string PendingPrefs = "PendingPrefs";private Dictionary<string, ProductData> pendingProducts = new Dictionary<string, ProductData>();public void Initialize(List<ProductDefinition> products){if (IsInitialized()){return;}// Create a builder, first passing in a suite of Unity provided stores.var module = StandardPurchasingModule.Instance();module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;var builder = ConfigurationBuilder.Instance(module);builder.AddProducts(products);//调用 UnityPurchasing.Initialize 方法可启动初始化过程,从而提供监听器的实现和配置。//请注意,如果网络不可用,初始化不会失败;Unity IAP 将继续尝试在后台初始化。仅在 Unity IAP 遇到无法恢复的问题(例如配置错误或在设备设置中禁用 IAP)时,初始化才会失败。//因此,Unity IAP 所需的初始化时间量可能是任意的;如果用户处于飞行模式,则会是无限期的时间。您应该相应地设计您的应用商店,防止用户在初始化未成功完成时尝试购物。UnityPurchasing.Initialize(this, builder);InitPendingOrder();}private bool IsInitialized(){// Only say we are initialized if both the Purchasing references are set.return m_StoreController != null && m_StoreExtensionProvider != null;}// Notice how we use the general product identifier in spite of this ID being mapped to// custom store-specific identifiers above.public void BuyProduct(string productId){// If Purchasing has been initialized ...if (IsInitialized()){// ... look up the Product reference with the general product identifier and the Purchasing // system's products collection.Product product = m_StoreController.products.WithID(productId);// If the look up found a product for this device's store and that product is ready to be sold ... if (product != null && product.availableToPurchase){Debug.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));// ... buy the product. Expect a response either through ProcessPurchase or OnPurchaseFailed asynchronously.m_StoreController.InitiatePurchase(product);}// Otherwise ...else{// ... report the product look-up failure situation  Debug.Log("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");OnPurchaseFailedEvent?.Invoke((int)PurchaseFailureReason.ProductUnavailable, ProductData.FromProduct(product));}}else{// ... report the fact Purchasing has not succeeded initializing yet. Consider waiting longer or // retrying initiailization.Debug.Log("BuyProductID FAIL. Not initialized.");OnPurchaseFailedEvent?.Invoke((int)PurchaseFailureReason.PurchasingUnavailable,null);}}public void CancelPurchase(){if (!string.IsNullOrEmpty(purchasingProductId)){Product product = m_StoreController.products.WithID(purchasingProductId);if (product != null && product.availableToPurchase){m_StoreController.ConfirmPendingPurchase(product);purchasingProductId = null;}}}// Restore purchases previously made by this customer. Some platforms automatically restore purchases, like Google. // Apple currently requires explicit purchase restoration for IAP, conditionally displaying a password prompt.public void RestorePurchases(){// If Purchasing has not yet been set up ...if (!IsInitialized()){// ... report the situation and stop restoring. Consider either waiting longer, or retrying initialization.Debug.Log("RestorePurchases FAIL. Not initialized.");OnPurchaseFailedEvent?.Invoke((int)PurchaseFailureReason.PurchasingUnavailable,null);return;}// If we are running on an Apple device ... if (Application.platform == RuntimePlatform.IPhonePlayer ||Application.platform == RuntimePlatform.OSXPlayer){// ... begin restoring purchasesDebug.Log("RestorePurchases started ...");// Fetch the Apple store-specific subsystem.var apple = m_StoreExtensionProvider.GetExtension<IAppleExtensions>();// Begin the asynchronous process of restoring purchases. Expect a confirmation response in // the Action<bool> below, and ProcessPurchase if there are previously purchased products to restore.apple.RestoreTransactions((result) =>{// The first phase of restoration. If no more responses are received on ProcessPurchase then // no purchases are available to be restored.Debug.Log("RestorePurchases continuing: " + result +". If no further messages, no purchases available to restore.");});}// Otherwise ...else{// We are not running on an Apple device. No work is necessary to restore purchases.Debug.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);}}#region IStoreListener/// <summary>/// 初始化成功/// </summary>/// <param name="controller"></param>/// <param name="extensions"></param>public void OnInitialized(IStoreController controller, IExtensionProvider extensions){// Purchasing has succeeded initializing. Collect our Purchasing references.Debug.Log("OnInitialized: PASS");// Overall Purchasing system, configured with products for this application.m_StoreController = controller;// Store specific subsystem, for accessing device-specific store features.m_StoreExtensionProvider = extensions;OnInitializedEvent?.Invoke(controller.products.all);#if LOGGER_ONStringBuilder sb = new StringBuilder();sb.Append("内购列表展示 -> count:" + controller.products.all.Length + "\n");foreach (var item in controller.products.all){if (item.availableToPurchase){sb.Append("localizedPriceString :" + item.metadata.localizedPriceString + "\n" +"localizedTitle :" + item.metadata.localizedTitle + "\n" +"localizedDescription :" + item.metadata.localizedDescription + "\n" +"isoCurrencyCode :" + item.metadata.isoCurrencyCode + "\n" +"localizedPrice :" + item.metadata.localizedPrice + "\n" +"type :" + item.definition.type + "\n" +"receipt :" + item.receipt + "\n" +"enabled :" + (item.definition.enabled ? "enabled" : "disabled") + "\n \n");}}Debug.Log(sb.ToString());
#endif}/// <summary>/// 初始化失败/// </summary>/// <param name="error"></param>public void OnInitializeFailed(InitializationFailureReason error){// Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);purchasingProductId = null;OnInitializeFailedEvent?.Invoke((int) error);}/// <summary>/// 购买成功/// </summary>/// <param name="args"></param>/// <returns></returns>public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args){purchasingProductId = null;var pdata = ProductData.FromProduct(args.purchasedProduct);// A product has been purchased by this user.OnPurchaseSuccessEvent?.Invoke(pdata);AddPendingOrder(pdata);// Return a flag indicating whether this product has completely been received, or if the application needs // to be reminded of this purchase at next app launch. Use PurchaseProcessingResult.Pending when still // saving purchased products to the cloud, and when that save is delayed. return PurchaseProcessingResult.Complete;}/// <summary>/// 购买失败/// </summary>/// <param name="product"></param>/// <param name="failureReason"></param>public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason){// A product purchase attempt did not succeed. Check failureReason for more detail. Consider sharing // this reason with the user to guide their troubleshooting actions.Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}",product.definition.storeSpecificId, failureReason));OnPurchaseFailedEvent?.Invoke((int) failureReason, ProductData.FromProduct(product));}#endregion#region 二次验证/// <summary>/// 初始化未完成订单/// </summary>private void InitPendingOrder(){string encrypt = PlayerPrefs.GetString(PendingPrefs);if (!string.IsNullOrEmpty(encrypt)){string json = EncryptUtility.Decrypt(encrypt);pendingProducts = JsonMapper.ToObject<Dictionary<string, ProductData>>(json);Logger.Log("[IAP] InitPendingOrder:" + json);Logger.Log("[IAP] pendingProducts.Count:" + pendingProducts.Count);}}/// <summary>/// 添加未完成订单,等待服务器验证/// </summary>/// <param name="product"></param>private void AddPendingOrder(ProductData product){if (!pendingProducts.ContainsKey(product.transactionID)){pendingProducts.Add(product.transactionID, product);string json = JsonMapper.ToJson(pendingProducts);string encrypt = EncryptUtility.Encrypt(json);PlayerPrefs.SetString(PendingPrefs, encrypt);}}/// <summary>/// 已完成验证的删除订单/// </summary>/// <param name="product"></param>private void RemovePendingOrder(ProductData product){if (pendingProducts.ContainsKey(product.transactionID)){pendingProducts.Remove(product.transactionID);string json = JsonMapper.ToJson(pendingProducts);string encrypt = EncryptUtility.Encrypt(json);PlayerPrefs.SetString(PendingPrefs, encrypt);}}public int GetPendingOrderCount(){return pendingProducts.Count;}/// <summary>/// 通知server进行订单收据的二次验证/// </summary>/// <param name="userID">用户id</param>/// <param name="product">商品</param>/// <param name="callback">回调</param>public void ReceiptVerify(string userID, ProductData product, Action<int> callback){Dictionary<string, string> dic = new Dictionary<string, string>();dic.Add("userID", userID);dic.Add("receipt", product.receipt);string args = JsonMapper.ToJson(dic);Debug.LogWarning($"[IAP.Req]: {VerifyURL}?{args}");NetworkHttp.Instance.Post(VerifyURL, null, ByteUtility.StringToBytes(args), (response) =>{if (response == null){callback?.Invoke(408);return;}var result = JsonMapper.ToObject(response);if (result == null || !result.ContainsKey("code")){callback?.Invoke(408);Logger.LogError("[IAP.Verify] with err : result is null!");return;}var code = Convert.ToInt32(result["code"].ToString());if (code != 0){callback?.Invoke(code);Logger.LogError("[IAP.Verify] with err : {0}", result["msg"]);return;}RemovePendingOrder(product);callback?.Invoke(200);}, 15);}/// <summary>/// 检测待办订单列表/// </summary>/// <param name="userID">用户id</param>/// <param name="callback">回调</param>public void CheckPendingOrder(string userID, Action<int, ProductData> callback){var count = pendingProducts.Count;if (count == 0){return;}foreach (var pair in pendingProducts){ReceiptVerify(userID, pair.Value, code => { callback?.Invoke(code, pair.Value); });break;}}#endregion}
}

八.参考链接:

https://learn.unity.com/tutorial/unity-iap

https://docs.unity3d.com/cn/2020.3/Manual/UnityIAPInitialization.html

https://developer.apple.com/documentation/appstorereceipts

Unity如何接入应用内购In-AppPurchase相关推荐

  1. Unity接入苹果内购

    文章目录 前言 一.苹果内购是什么? 二.Unity接入内购 1.开启内购 2.使用内购 总结 前言 Unity接入苹果内购前,需要提前配置好产品的内购ID以及拥有苹果账号,关于这个提前准备各位需要自 ...

  2. Unity接入苹果内购(IAP)

    Unity接入苹果内购(IAP) 前言 苹果支付流程 配置App 配置商品 协议.税务和银行业务 沙盒测试账号 Unity(IAP) 测试 前言 第一次发帖,有点激动嘿嘿!话不多说直接奔主题,项目中需 ...

  3. 真正手把手教你用unity接入苹果内购(IAP)

    原帖:真正手把手教你用unity接入苹果内购(IAP) http://www.manew.com/thread-100403-1-1.html (出处: -[游戏蛮牛]-ar增强现实,虚拟现实,uni ...

  4. Unity接入三星内购

    文章目录 前言 一.SDK下载 二.接入步骤 1.导入SDK 2.请求商业卖家状态 3.注册应用程序 4.添加应用内商品 5.添加测试用户 总结 前言 因公司需要,接入三星内购,Unity是支持三星商 ...

  5. Unity接入iOS内购

    1.内购种类 consumable:可消费的,如游戏中的金币,用完还可以再购买. non-consumable:不可销毁的,一次购买,永久生效.比如去广告,解锁游戏关卡,这种商品只能购买一次. sub ...

  6. Unity接入GooglePlay内购V4(源生Android方式)

    Unity接GooglePlay In-App Billing坑还是蛮多的,各种坑. 接的方式目前来看有三种: 采用Unity IAP插件,开启Unity的IAP Service 采用Android源 ...

  7. C#接入steam内购

    这一篇文章主要记录了C#服务端唤起steam交易授权页面时遇到的问题. 问题主要在于steam文档太难读了. 这一段是重点: 第一步:获取客户端请求生成订单时,要先请求GetUserInfo接口 请求 ...

  8. Unity接入OneStore内购

    前言 OneStore是韩国第一大android应用市场,访问官网可能需要你科学上网才能正常访问. [中文]OneStore开发工具 [中文]在Unity中使用ONE store In-App支付 国 ...

  9. Unity 之 Mac App Store 内购过程解析(购买非消耗道具 | 恢复购买 | 支付验证)

    Unity 之 Mac App Store 内购过程解析(恢复购买) 准备工作 一,具体实现 1.1 场景搭建 1.2 代码实现 1.3 打包设置 二,打包测试 2.1 实现步骤说明 2.2 Mac签 ...

最新文章

  1. html5下拉智能,HTML5新增标签 + 智能表单
  2. mysql windows乱码_小白楠--windows系统下mysql乱码
  3. java语言jdk_Java语言环境(JDK的安装教学)
  4. 计算机组成原理作业3,兰州大学《计算机组成原理》13春在线作业3
  5. Vue简单入门及组件的简单使用
  6. 班尼机器人怎么语音_每日一句中话西说巧学英语:“我们今天就到这儿吧。”英语怎么说?...
  7. telnet批量ip端口测试连通自动脚本
  8. 重磅 | 企业大数据战略规划,看这一篇文章就够了!
  9. Excel如何转化成PDF?教你几个简单的方法
  10. 外卖和快递行业数据_白领市场三分天下,外卖行业将何去何从?
  11. android毫秒数转换为时分秒,如何将毫秒转换成单独的时分秒的形式?
  12. Windows配置域名
  13. 安徽大学计算机复试刷人比例,658人进复试刷掉564多人!盘点21复试比奇高、刷人狠的院校专业...
  14. CF Edu 53D Berland Fair 暴力,取模
  15. android+隐藏邮件地址,“通过 Apple 登录”功能的“隐藏邮件地址”
  16. 【阅读笔记】项亮前辈的《推荐系统实战》
  17. 教你找回直接打开outlook附件文件编辑后保存但未另存为的附件文件?
  18. 【No7.】Android 像素转换工具
  19. 自动化学报latex模板使用说明
  20. cancase vector_低價替代Vector CANoe CAN總線適配解決方案支持所有USBCAN(周立功CAN、PCAN、Kvaser、ValueCAN、NI CAN)...

热门文章

  1. 安装MS15-034漏洞补丁KB3042553失败
  2. e.g., malformed request syntax, invalid request message framing, or deceptive request routing
  3. 科技云报道:2021《分布式文件系统和对象存储魔力象限图》解读
  4. IconWorkshop英文版(带激活工具)
  5. 【听】红高粱,莫言经典诺贝尔文学奖小说
  6. 怎么用计算机打开开发者模式,win10如何打开开发者选项模式
  7. C++打开网页,发起QQ对话,调用外部exe程序
  8. Virual Studio 2022 C++ CLR 中 模拟 Android Studo,Eclipse 的 LogCat
  9. 扫描二维码下载app,判断是Android还是ios,并跳转到不同的下载地址
  10. uni-app触发点击事件