• 【多服务场景化解决方案】智能家居(UrbanHome)


    介绍

    UrbanHome是一款提供房屋维修服务的移动应用。如有维修需求,用户可通过该应用联系所在城市的管道工,电工,保洁,漆匠,木匠,修理工等,或是搜寻导航附近的维修商店。

    通过构建UrbanHome这款应用,您可以实现以下华为移动服务的功能:

    1. 通过账号服务完成用户验证。

    2. 通过云数据库,不同城市的维修者们能够增、删、改、查自己的信息。

    3. 通过用户身份服务,用户可以管理多个地址并基于当前地址通过云数据库查看维修者信息。

    4. 通过位置服务,定位服务和地图服务,用户可以搜索并导航附近的维修店。

    您将建立什么

    在这个Codelab中,您将会建立一个集成了账号服务,地图服务、定位服务、位置服务、云数据库和用户身份服务的项目。在该项目中,您可以尝试:

    • 使用账号服务实现华为账号登录。

    cke_755375.png

    • 使用云数据库管理数据。

    cke_765671.png

    • 使用定位服务获取当前位置的坐标并使用地图服务绘制用户前往修理店的路线。

    cke_780131.png

    • 使用用户身份服务管理地址。

    cke_794742.png

    • 使用位置服务搜索附近修理店。

    cke_807776.png

    您将学到什么

    在这个Codelab中,您将学到:

    • 如何使用账号服务实现华为账号登录。
    • 如何使用定位服务获取用户当前位置。
    • 如何使用地图服务在HMS地图上定位消费者所在位置。
    • 如何使用位置服务获取周边修理店位置。
    • 如何使用云数据库为修理者提供信息的增、删、改、查操作。
    • 如何使用用户身份服务为用户提供地址的增、删、改、查操作。

     

    您需要什么

    说明:需要一个华为帐号,并且此账号身份已验证。

    硬件要求

    请提前准备上述硬件环境和相关设备。

    • 运行Windows 10操作系统的台式机或笔记本电脑。
    • 安装HMS Core (APK) 5.1.0.309或以上版本的华为手机一部。

    软件要求

    请提前准备上述软件环境。

    • Android Studio 4.X
    • JDK 1.8或以上。
    • SDK Platform 28或以上。
    • Gradle 4.6或以上。

    能力接入准备

    要集成HMS Core相关服务,需要完成以下准备:

    • 登录AppGallery Connect并创建应用。
    • 创建Android Studio工程。
    • 生成签名证书。
    • 生成签名证书指纹。
    • 配置签名证书指纹。
    • 添加应用包名并保存配置文件。
    • 在项目级build.gradle文件中,添加AppGallery Connect插件以及Maven仓地址。
    • 在Android Studio中配置签名证书。

    具体操作,请按照HMS Core集成准备中的详细说明来完成。

    启用相关服务

    前往“项目设置”页面,选择“API管理”并开通下述服务。

    说明:部分API默认是关闭的,您必须手动启用。

    • 账号服务
    • 定位服务
    • 位置服务
    • 地图服务

    cke_821010.png

    至此,您已成功启用应用所需的华为服务。

    集成账号服务

    通过华为帐号开放服务,Android应用可以方便和安全地实现快速登录授权、读取短信验证码等功能。凭借用户授权凭证(即Access Token),应用可快速调用华为开放接口。

    在依赖代码块中添加账号服务SDK的依赖。

    implementation 'com.huawei.hms:hwid:5.0.3.301'

    使用账号服务实现华为账号登录

    1.申请授权,获取Token。

    若用户选择使用华为账号登录应用,请调用如下startAuthService()方法启动授权。

    /**
     * Start AuthService and request the scope parameters
     */
    private fun startAuthService() {
        HuaweiIdAuthParamsHelper(HuaweiIdAuthParams.DEFAULT_AUTH_REQUEST_PARAM)
            .setUid()
            .setProfile()
            .setMobileNumber()
            .setEmail()
            .setIdToken()
            .setAccessToken()
            .setAuthorizationCode()
            .setScopeList(scopes)
            .createParams()
        startActivityForResult(service.signInIntent, AppConstants.LOGIN_AUTH_CODE)
    }

    2.调用如下回调方法检测用户授权是否成功。

    说明:更多详细信息请参考账号服务接入流程

    /**
    *Check authentication is successful or not.
    */
    override fun onActivityResult(requestCode: Int, resultCode: Int, @Nullable data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == RESULT_OK)
            when (requestCode) {
                AppConstants.LOGIN_AUTH_CODE -> {
                    AGConnectAuth.getInstance().signOut()
                    val authHuaweiIdTask: Task =
                        HuaweiIdAuthManager.parseAuthResultFromIntent(data)
                    if (authHuaweiIdTask.isSuccessful) {
                        huaweiAccount = authHuaweiIdTask.result
                        Log.d(
                            TAG,
                            AppConstants.LOGIN_GET_ACCESS_TOKEN
                        )
                        val credential =                     HwIdAuthProvider.credentialWithToken(huaweiAccount?.accessToken)
                        agConnectAuth.signIn(credential)
                            ?.addOnSuccessListener {
                                it.user.displayName
                                it.user.email
                                it.user.uid
                                it.user.providerInfo
                                mCloudDBZoneWrapper
                                    .setmUiCallBack(this)
                                mCloudDBZoneWrapper
                                    .openCloudDBZoneV2()
                            }
                            ?.addOnFailureListener {
                                Log.e(TAG, AppConstants.LOGIN_FAILED)
                            }
                    } else {
                        Log.e(
                            TAG,
                            AppConstants.LOGIN_FAILED
                        )
                    }
                }

    cke_836834.png

    接入地图服务

    地图服务给您提供一套地图开发调用的SDK,地图数据覆盖超过200个国家和地区,支持70多种地图展示与搜索语言,方便您轻松地在应用中集成地图相关的功能,全方位提升用户体验。

    在本项目中,该服务用于在地图上展示用户当前位置和修理店位置并为用户规划路线。

    接入地图服务具体如下:

    1.添加下述依赖集成地图服务。

    implementation 'com.huawei.hms:maps:5.0.5.301'

    2.初始化Map View。

    MapsInitializer.setApiKey(AppConstants.API_KEY)
    mMapView.onCreate(mapViewBundle)
    mMapView.getMapAsync(this)

    3.加载Map View。

    /**
    *To load Map View
    */
    Override fun onMapReady(huaweiMap: HuaweiMap) {
         latLng1 = LatLng(Utils.curentLatitude, Utils.currentLongitude)
         latLng2 = lat?.let { lng?.let { it1 -> LatLng(it, it1) } }
         hMap = huaweiMap
         hMap?.isMyLocationEnabled = true
         val build = CameraPosition.Builder().target(latLng1).zoom(3f).tilt(45f).build()
         val cameraUpdate = CameraUpdateFactory.newCameraPosition(build)
         hMap?.apply {
             animateCamera(cameraUpdate)
             moveCamera(cameraUpdate)
         }
         addOriginMarker(latLng1!!)
         latLng2?.let { addDestinationMarker(it) }
         removePolylines()
         hMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng1, 10f))
         mMarkerOrigin?.showInfoWindow()
         hMap?.apply {
             moveCamera(CameraUpdateFactory.newLatLngZoom(latLng2, 10f))
             resetMinMaxZoomPreference()
         }
         mMarkerDestination?.showInfoWindow()
         
     }

    4.在地图上展示用户/修理者位置。

    说明:更多详细信息请参考地图服务接入流程

    /**
    * This method shows a marker for Consumer location on Map.
    */
    private fun addOriginMarker(latLng: LatLng) {
         if (null != mMarkerOrigin) {
             mMarkerOrigin?.remove()
         }
         val address = getCompleteAddressString(Utils.curentLatitude, Utils.currentLongitude)
         mMarkerOrigin = hMap?.addMarker(
             MarkerOptions().position(latLng)
                 .anchorMarker(0.5f, 0.9f)
                 .title("Current Location")
                 .snippet(address)
         )
     }

    cke_852739.png

    集成定位服务

    Android定位SDK采用全球卫星导航系统(Global Navigation Satellite System,简称GNSS)、Wi-Fi、基站等多途径的混合定位模式进行定位,赋予您的应用灵活的全球定位能力。目前,该服务提供融合定位、活动识别、地理围栏等主要能力。

    1.添加下述依赖集成定位服务。

    implementation 'com.huawei.hms:location:5.0.4.300'

    2.获取用户当前位置坐标(经度和纬度)。

    /**
    *This method shows a marker for Service Provider location on HMS Map
    */
    private fun addDestinationMarker(latLng: LatLng) {
         if (null != mMarkerDestination) {
             mMarkerDestination?.remove()
         }
         mMarkerDestination = hMap?.addMarker(
             MarkerOptions().position(latLng).anchorMarker(0.5f, 0.9f).title(storeName)
                 .snippet(storeAddress)
         )
     }

    3.在Android manifest文件中添加位置权限。

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

    4.获取用户当前更新位置坐标(经度和纬度)。

    /**
    *This method fetches location updates
    */
    private fun startLocationUpdates() {
         fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, null)
     }
    
    private val locationCallback = object : LocationCallback() {
         override fun onLocationResult(locationResult: LocationResult?) {
             locationResult ?: return
             for (location in locationResult.locations) {
                 setLocationData(location)
             }
         }
     }
    
     /**
    * To set location data.
    */
    private fun setLocationData(location: Location) {
         value = LocationModel(longitude = location.longitude, latitude = location.latitude)
     }

    5.获取最新位置信息。

    说明:更多详细信息请参考定位服务接入流程

    /**
    * To fetch last location information
    */
    override fun onActive() {
         super.onActive()
         fusedLocationClient.lastLocation
             .addOnSuccessListener { location: Location? ->
                 location?.also {
                     setLocationData(it)
                 }
             }
         startLocationUpdates()
     }

    集成用户身份服务

    用户身份服务为用户提供统一的地址管理服务,包括地址录入、编辑、删除和查询,支持用户一键授权应用使用地址信息,高效便利。

    1.添加下述依赖集成用户身份服务。

    implementation 'com.huawei.hms:identity:5.1.0.300'

    2.通过用户身份服务获取用户地址信息。

    /**
    * To fetch user address from Identity kit
    */
     
    private fun getUserAddress() {
         val task = Address.getAddressClient(this@MainActivity).getUserAddress(UserAddressRequest())
         task.addOnSuccessListener {
             Log.i(TAG, AppConstants.LOGIN_USER_DATA_SUCCESS)
             try {
                 startActivityForResult(it)
             } catch (ex: IntentSender.SendIntentException) {
                 Log.d(TAG, "SendIntentException")
             }
         }.addOnFailureListener {
             Log.i(TAG, AppConstants.LOGIN_USER_DATA_FAILED)
         }
     }

    3.在用户首次登录时调用下述回调从用户身份服务提供的地址列表中选择地址。

    /**
    * To fetch user address result.
    */
    private fun startActivityForResult(result: GetUserAddressResult) {
         val status = result.status
         if (result.returnCode == 0 && status.hasResolution()) {
             Log.i(TAG, AppConstants.LOGIN_RESULT_RES)
             status.startResolutionForResult(
                 this@MainActivity, AppConstants.LOGIN_GET_ADDRESS_REQUESTCODE
             )
         } else {
             Log.i(TAG, AppConstants.LOGIN_RESULT_RES_FAILED)
             Utils.showToast(this@MainActivity, getString(R.string.msg_failed_user_resolution))
         }
     }

    4.读取用户地址。

    说明:更新详细信息,请参考用户身份服务接入流程

    /**
    * To read user address result.
    */
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
         super.onActivityResult(requestCode, resultCode, data)
         when(requestCode) {
             AppConstants.LOGIN_GET_ADDRESS_REQUESTCODE -> {
                 onGetAddressResult(resultCode, data)
                 fragmentCommunicator?.passDataToFragment(data)
             }
         }
     }

    cke_870584.png

    集成位置服务

    位置服务提供位置查询服务,帮助您的用户更加方便地使用位置相关服务,包括关键字搜索、周边搜索、地点详情、及地点搜索建议等,有助于您的App吸引更多用户,提升用户粘性。

    1.添加下述依赖集成位置服务。

    implementation 'com.huawei.hms:site:5.0.2.300'

    2.初始化位置服务。

    searchService = SearchServiceFactory.create(this, Utils.getApiKey())

    3.通过用户身份服务获取用户地址信息。

    说明:更多详细信息,请参考位置服务接入流程

    /**
    *To fetch nearby stores based on user's current location
    */
    intent.let {
         val request = NearbySearchRequest().apply {
             queryString=it.getStringExtra(AppConstants.REQUEST_QUERY).toString()
             setQuery(queryString)
             setLocation(
                 Coordinate(
                     it.getDoubleExtra(AppConstants.SERVICE_LAT_KEY, 0.0),
                     it.getDoubleExtra(AppConstants.SERVICE_LNG_KEY, 0.0)
                 )
             )
         }
         imageString = it.getStringExtra(AppConstants.PROVIDER_IMAGE_KEY).toString()
         searchService?.nearbySearch(request, searchResultListener)
     }

    cke_889359.png

    集成云数据库

    在这个步骤中,您可以学到:

    • 如何使用云数据库开发应用
    • 如何向云数据库中写入应用数据
    • 如何查询数据
    • 如何实时监听数据变化
    • 端侧和云侧的数据同步
    • 华为云数据库是一款端云协同的数据库产品,提供端云数据的协同管理、统一的数据模型和丰富的数据管理API接口等能力。

    该云数据库十分契合本工程项目,有利于我们进行数据的增、删、查、改操作。

    开发准备

    使用云数据库构建应用服务需完成下述准备工作:

    • 在AppGallery Connect注册账号并通过实名认证。
    • 登录AppGallery Connect创建项目及应用。
    • 开通AppGallery Connect匿名帐号认证服务,使应用有认证用户的相关权限。
    • 本地上安装Android Studio。

    开通服务

    使用云数据库前,您需要先开通服务。

    1.登录AppGallery Connect,点击“我的项目”。

    2.在项目列表中选择项目和需要开通云数据库的应用。

    3.在导航树上选择“构建”,点击“云数据库”。

    4.点击“立即开通”。 

    cke_905689.png

    5.如果您之前没有选择数据处理位置,开通云数据库后首先将会设置数据处理位置。 

    cke_919451.png

    6.云数据库初始化完成后,该服务成功开通。

    新增和导出对象类型

    下述示例将展示如果在AppGallery Connect上新增和导出用于Android应用开发的java格式对象类型文件。

    1.登录AppGallery Connect,选择“我的项目”。

    2.在项目列表页面中选择项目,单击项目下需要创建对象类型的应用。

    3.在导航树上选择“构建”,点击“云数据库”。

    4.点击“新增”,进入创建对象类型页面。

    cke_933347.png

    5.输入对象类型名为LoginInfo后,点击“下一步”。

    6.点击“+新增字段”,新增如下字段后,单击“下一步”。

    字段名称

    类型

    主键

    非空

    加密

    默认值

    user_id

    Integer

    user_email

    String

    user_name

    String

    user_phone

    Double

    photo_uri

    String

    device_token

    Date

    shadowFlag

    Boolean

    true

    7.(可选)点击“新增索引”。

    8.按照如下要求设置各角色权限后,点击“下一步”。

    角色

    query

    upsert

    delete

    所有人

    认证用户

    数据创建者

    管理员

    9.点击“确定”。

    在对象类型列表中可以看到已创建的对象类型。

    重复上述步骤,完成Service type、Service Category对象类型的创建。

    10.点击“导出”。

    cke_971517.png

    11.设置导出文件格式为java格式。

    12.设置java文件类型为“android”。

    13.输入java文件中的包名。

    包名只能包含以下3种字符:

    字母(A-Z或a-z)

    数字(0-9)

    特殊字符:_和.

    14.    点击“导出”。

    文件将会导出至本地,其内包含该版本中所有的对象类型。导出的java格式文件在后续步骤用于添加至本地开发环境。

    新增存储区

    您可基于AppGallery Connect在云侧创建数据存储区,请您遵循操作步骤创建一个存储区名称为“UrbanHomeServices”的存储区。

    1.登录AppGallery Connect,点击“我的项目”。

    2.在项目列表页面中选择项目,单击项目下需要创建存储区的应用。

    3.在导航树上选择“构建 > 云数据库”。

    4.选择“存储区”页签。

    5.点击“新增”,进入创建存储区页面。 

    cke_997362.png

    6.输入存储区名称为“UrbanHomeSevices”。

    7.点击“确定”。

    8.创建完成后返回存储区列表中,可以查看已创建的存储区。

    配置开发环境

    1.在项目级的/app/build.gradle文件中dependencies节点添加Cloud DB SDK。

    implementation 'com.huawei.agconnect:agconnect=database:1.2.3.301'

    2.在build.gradle文件中设置Java源码兼容模式为JDK1.8版本。

    compileOptions {
    sourceCompatibility = 1.8
    targetCompatibility = 1.8
    }

    添加对象类型文件

    本地应用开发无需再次创建对象类型。

    1.将已在AppGallery Connect上导出的全部java格式文件添加至本地开发环境。

    2.Initialize Cloud DB. 通过AGConnectCloudDB类中的createObjectType()方法实现对象类型的定义和创建。

    初始化

    在添加对象类型文件后,您就可以使用云数据库进行应用开发。开发应用时,需要先初始化AGConnectCloudDB,然后创建Cloud DB zone和对象类型。

    1.在应用的CloudDBZoneWrapper类中初始化AGConnectCloudDB。

    /**
    * To initialize AGConnectCloudDB
    */
    public static void initAGConnectCloudDB(Context context) { 
        AGConnectCloudDB.initialize(context); 
    }

    2.获取AGConnectCloudDB实例和创建对象类型。

    mCloudDB = AGConnectCloudDB.getInstance(); 
    mCloudDB.createObjectType(ObjectTypeInfoHelper.getObjectTypeInfo());

    3.创建Cloud DB zone配置对象,并打开该Cloud DB zone。

    /**
    * This method is used to open Cloud DB zone.
    */
    public void openCloudDBZoneV2() {
         mConfig = new CloudDBZoneConfig(AppConstants.URBAN_HOME_SERVICES,
     CloudDBZoneConfig.CloudDBZoneSyncProperty.CLOUDDBZONE_CLOUD_CACHE, CloudDBZoneConfig.CloudDBZoneAccessProperty.CLOUDDBZONE_PUBLIC);
         mConfig.setPersistenceEnabled(true);
         Task openDBZoneTask = mCloudDB.openCloudDBZone2(mConfig, true);
         openDBZoneTask.addOnSuccessListener(cloudDBZone -> {
             Log.w(TAG, "open clouddbzone success");
             mCloudDBZone = cloudDBZone;
             // Add subscription after opening cloudDBZone success
             mUiCallBack.onInitCloud();
             addSubscription();
         }).addOnFailureListener(e ->
                 Log.w(TAG, "open clouddbzone failed for"));
     }

    cke_423080.gif写入数据

    您可以使用executeUpsert()接口向当前Cloud DB zone中写入一个或一组对象。

    /**
    * This method is used to insert data into Cloud DB.
    */
    public void insertDbZoneInfo(T objectInfo) {
         if (mCloudDBZone == null) {
             Log.w(TAG, "CloudDBZone is null, try re-open it");
             return;
         }
         Task upsertTask = mCloudDBZone.executeUpsert(objectInfo);
         upsertTask.addOnSuccessListener(cloudDBZoneResult -> {
             mUiCallBack.onInsertSuccess(cloudDBZoneResult);
         }).addOnFailureListener(e -> {
             mUiCallBack.updateUiOnError("Insert table info failed");
         });
     }

    查看数据

    用户在应用界面中新增的数据,将会被存储在云侧。在端侧注册数据变化监听器,当云侧数据发生变化时,端侧能够感知数据变化,及时刷新本地应用数据。

    调用subscribeSnapshot()方法并设置查询条件可以指定监听对象。当监听对象的数据发生变化时,端侧会收到通知,根据快照获取数据变化信息,从云侧同步数据至端侧应用。

    /**
    * This listener is used to get snapshot
    */
    private OnSnapshotListener mSnapshotListener = (cloudDBZoneSnapshot, e) -> {
         if (e != null) {
             Log.w(TAG, "onSnapshot" );
             return;
         }
         CloudDBZoneObjectList snapshotObjects = cloudDBZoneSnapshot.getSnapshotObjects();
         List dbZoneList = new ArrayList<>();
         try {
             if (snapshotObjects != null) {
                 while (snapshotObjects.hasNext()) {
                     T objectInfo = snapshotObjects.next();
                     dbZoneList.add(objectInfo);
                 }
             }
             mUiCallBack.onSubscribe(dbZoneList);
         } catch (AGConnectCloudDBException snapshotException) {
             Log.w(TAG, "onSnapshot:(getObject)");
         } finally {
             cloudDBZoneSnapshot.release();
         }
     };

    查询数据

    您可以通过executeQuery()addOnSuccessListener()addOnFailureListener()方法组合,实现异步方式查询数据。

    /**
    * This method is used to query all data from Cloud DB.
    */
    public void queryAllData(CloudDBZoneQuery query) {
         if (mCloudDBZone == null) {
             Log.w(TAG, "CloudDBZone is null, try re-open it");
             return;
         }
         Task<CloudDBZoneSnapshot> queryTask = mCloudDBZone.executeQuery(query,
                 CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_ONLY);
         queryTask.addOnSuccessListener(new OnSuccessListener<CloudDBZoneSnapshot>() {
             @Override
             public void onSuccess(CloudDBZoneSnapshot snapshot) {
                 processQueryResult(snapshot);
             }
         }).addOnFailureListener(new OnFailureListener() {
             @Override
             public void onFailure(Exception e) {
                 mUiCallBack.updateUiOnError("Query failed");
             }
         });
     }

    删除数据

    您可以使用executeDelete()方法删除一个或一组对象。删除数据时,Cloud DB会根据传入对象主键删除相应的数据,不会比对该对象其它属性与存储的数据是否一致。删除一组对象时,删除操作是原子的,即对象列表中的对象要么全部删除成功,要么全部删除失败。

    /**
    * This method is used delete table data on Cloud DB.
    */
     public void deleteTableData(List tableObject) {
             if (mCloudDBZone == null) {
                 Log.w(TAG, "CloudDBZone is null, try re-open it");
                 return;
             }
             Task deleteTask = mCloudDBZone.executeDelete(tableObject);
             if (deleteTask.getException() != null) {
                 mUiCallBack.updateUiOnError("Delete service type table failed");
                 return;
             }
             mUiCallBack.onDelete(tableObject);
         }
     }

    编辑数据

    您可以使用editService()方法编辑修理工的信息。

    /**
    * This method is used to edit Service details.
    */
    override fun editService(listObject: ServiceType) {
         val intent = Intent(this, AddServiceActivity::class.java)
         intent.apply {
             putExtra(AppConstants.CATEGORY_NAME, listObject.cat_name)
             putExtra(AppConstants.PROVIDER_PH_NUM, listObject.phone_number.toString())
             putExtra(AppConstants.PROVIDER_MAIL_ID, listObject.email_id)
             putExtra(AppConstants.PROVIDER_COUNTRY, listObject.country)
             putExtra(AppConstants.PROVIDER_ID, listObject.id)
             putExtra(AppConstants.PROVIDER_NAME, listObject.service_provider_name)
             putExtra(AppConstants.PROVIDER_CITY, listObject.city)
             putExtra(AppConstants.PROVIDER_STATE, listObject.state)
         }
         startActivity(intent)
     }

    cke_1018241.png

    提示:

    • 请务必使用最新版本的依赖。
    • 请务必在对象类型中为用户分配合理的角色。

    说明:更多详细信息,请参考云数据库接入文档

    恭喜您

    祝贺您,您已成功构建UrbanHome。

    参考

    有关更多信息,请参阅以下官方文档。

    本Codelab中的示例代码下载地址如下:源码下载

    欲了解更多更全技术文章,欢迎访问https://developer.huawei.com/consumer/cn/forum/?ha_source=zzh

  • 相关阅读:
    手把手教会你视频转文字怎么弄,这个方法建议收藏备用
    河南分销小程序开发|商城小程序开发为什么会火?
    操作系统:中断和异常
    推荐一个python、AI学习社区:ShowMeAI
    关于Http和Https
    Linux线程同步(上)
    微信小程序瀑布流布局
    luffy-(9)
    编译原理复习——符号表
    STM32无硬件随机数发生器时生成随机数的方法
  • 原文地址:https://www.cnblogs.com/developer-huawei/p/16659344.html