ToB企服应用市场:ToB评测及商务社交产业平台

标题: 【HarmonyOS】鸿蒙应用蓝牙功能实现 (三) [打印本页]

作者: 汕尾海湾    时间: 前天 21:17
标题: 【HarmonyOS】鸿蒙应用蓝牙功能实现 (三)
【HarmonyOS】鸿蒙应用蓝牙功能实现 (三)

     前言

     今天整理蓝牙Demo代码,查看官网时发现自己帐号没有登录,竟然也可以访问最新的API文档,真是喜大奔普。看来华为已经开始对外开放最新的API文档,不再有白名单限定了。
     

     HarmonyOS NEXTDeveloper Beta5
     接下来解说蓝牙配对流程。低功耗蓝牙BLE模式将在下面章节解说。
     蓝牙配对业务流程

     1.设备进入可被发现模式: 起首,设备需要进入可被发现模式,这样附近的蓝牙设备才能识别到它。一方设备(如手机)会主动搜索附近的蓝牙设备,并列出全部可用的配对选项。
     2.选择并触发配对哀求: 用户从列表中选择想要连接的设备,并触发配对哀求。此时,双方设备会交换一系列的身份验证信息,以确保相互的身份安全无误。在这个过程中,可能会要求用户输入配对码(如PIN码)或在设备上确认配对哀求。
     3.身份验证和加密: 一旦身份验证通过,设备间就会创建安全的连接通道,这一过程称为“配对乐成”。配对完成后,设备之间的连接就创建了,它们可以开始传输数据。
     4.数据传输: 设备间通过蓝牙举行数据传输,可以传输音频、文件等多种类型的数据。
     5.断开连接: 当数据传输完成后,蓝牙设备可以断开连接。断开连接的操作可以通过设备上的按钮大概软件来实现。
     蓝牙配对通常是一次性的,即一旦设备乐成配对,它们会在后续的连接中自动识别并连接,无需再次举行配对过程(除非设备被重置或用户手动取消配对)
     以下是传统的蓝牙配对流程图仅供参考:
     

     通例蓝牙配对Demo

     

     Demo包括以下内容: 1.蓝牙权限开启 2.蓝牙开启/关闭 3.蓝牙扫描开启/关闭 4.蓝牙配对 5.蓝牙code协议确认
     蓝牙UI交互类
         
  1. import { access } from '@kit.ConnectivityKit';
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. import { BlueToothMgr } from '../manager/BlueToothMgr';
  4. import { abilityAccessCtrl, common } from '@kit.AbilityKit';
  5. import { connection } from '@kit.ConnectivityKit';
  6. import { map } from '@kit.ConnectivityKit';
  7. import { pbap } from '@kit.ConnectivityKit';
  8. import { HashMap } from '@kit.ArkTS';
  9. import { DeviceInfo } from '../info/DeviceInfo';
  10. import { promptAction } from '@kit.ArkUI';
  11. @Entry
  12. @Component
  13. struct Index {
  14.   private TAG: string = "BlueToothTest";
  15.   // 扫描状态定时器
  16.   private mNumInterval: number = -1;
  17.   // 当前设备蓝牙名
  18.   @State mCurrentDeviceName: string = "";
  19.   // 蓝牙状态
  20.   @State @Watch('onChangeBlueTooth') isStartBlueTooth: boolean = false;
  21.   // 蓝牙扫描状态
  22.   @State @Watch('onChangeBlueTooth') isStartScan: boolean = false;
  23.   // 当前蓝牙权限
  24.   @State userGrant: boolean = false;
  25.   // 扫描到设备名
  26.   @State mMapDevice: HashMap<string, DeviceInfo> = new HashMap();
  27.   // ui展现的设备列表
  28.   @State mListDeviceInfo: Array<DeviceInfo> = new Array();
  29.   async aboutToAppear() {
  30.     await this.requestBlueToothPermission();
  31.     let state = access.getState();
  32.     console.log(this.TAG, "getState state: " + state);
  33.     if(state == 2){
  34.       this.isStartBlueTooth = true;
  35.       console.log(this.TAG, "getState isStartBlueTooth: " + this.isStartBlueTooth);
  36.     }else{
  37.       this.isStartBlueTooth = false;
  38.       console.log(this.TAG, "getState isStartBlueTooth: " + this.isStartBlueTooth);
  39.     }
  40.   }
  41.   private onChangeBlueTooth(){
  42.     if(!this.isStartBlueTooth){
  43.       this.mMapDevice = new HashMap();
  44.       return;
  45.     }
  46.     this.mCurrentDeviceName = BlueToothMgr.Ins().getCurrentDeviceName();
  47.   }
  48.   // 用户申请权限
  49.   async reqPermissionsFromUser(): Promise<number[]> {
  50.     let context = getContext() as common.UIAbilityContext;
  51.     let atManager = abilityAccessCtrl.createAtManager();
  52.     let grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.ACCESS_BLUETOOTH']);
  53.     return grantStatus.authResults;
  54.   }
  55.   // 用户申请蓝牙权限
  56.   async requestBlueToothPermission() {
  57.     let grantStatus = await this.reqPermissionsFromUser();
  58.     for (let i = 0; i < grantStatus.length; i++) {
  59.       if (grantStatus[i] === 0) {
  60.         // 用户授权,可以继续访问目标操作
  61.         this.userGrant = true;
  62.         promptAction.showToast({ message: "蓝牙授权成功!"});
  63.       }else{
  64.         promptAction.showToast({ message: "蓝牙授权失败!"});
  65.       }
  66.     }
  67.   }
  68.   setBlueToothScan = ()=>{
  69.     if(!this.isStartScan){
  70.       promptAction.showToast({ message: "开启扫描!"});
  71.       BlueToothMgr.Ins().startScanDevice((data: Array<string>)=>{
  72.         let deviceId: string = data[0];
  73.         if(this.mMapDevice.hasKey(deviceId)){
  74.           // 重复设备,丢弃不处理
  75.         }else{
  76.           // 添加到表中
  77.           let deviceInfo: DeviceInfo = new DeviceInfo();
  78.           deviceInfo.deviceId = deviceId;
  79.           deviceInfo.deviceName = BlueToothMgr.Ins().getDeviceName(deviceId);
  80.           deviceInfo.deviceClass = BlueToothMgr.Ins().getDeviceClass(deviceId);
  81.           this.mMapDevice.set(deviceId, deviceInfo);
  82.           this.mListDeviceInfo = this.mListDeviceInfo.concat(deviceInfo);
  83.         }
  84.       });
  85.       this.mMapDevice.clear();
  86.       this.mListDeviceInfo = [];
  87.       // 开启定时器
  88.       this.mNumInterval = setInterval(()=>{
  89.         let discovering = BlueToothMgr.Ins().isCurrentDiscovering();
  90.         if(!discovering){
  91.           this.closeScanDevice();
  92.         }
  93.       }, 1000);
  94.       this.isStartScan = true;
  95.     }else{
  96.       promptAction.showToast({ message: "关闭扫描!"});
  97.       BlueToothMgr.Ins().stopScanDevice();
  98.       this.closeScanDevice();
  99.     }
  100.   }
  101.   private closeScanDevice(){
  102.     clearInterval(this.mNumInterval);
  103.     this.isStartScan = false;
  104.   }
  105.   setBlueToothState = ()=>{
  106.     try {
  107.       if(!this.isStartBlueTooth){
  108.         // 开启蓝牙
  109.         BlueToothMgr.Ins().setBlueToothAccess(true, (state: access.BluetoothState) => {
  110.           console.log(this.TAG, "getState setBlueToothAccessTrue: " + state);
  111.           if(state == access.BluetoothState.STATE_ON){
  112.             this.isStartBlueTooth = true;
  113.             promptAction.showToast({ message: "开启蓝牙!"});
  114.             console.log(this.TAG, "getState isStartBlueTooth: " + this.isStartBlueTooth);
  115.           }
  116.         });
  117.       }else{
  118.         BlueToothMgr.Ins().setBlueToothAccess(false, (state: access.BluetoothState) => {
  119.           console.log(this.TAG, "getState setBlueToothAccessFalse: " + state);
  120.           if(state == access.BluetoothState.STATE_OFF){
  121.             this.isStartBlueTooth = false;
  122.             promptAction.showToast({ message: "关闭蓝牙!"});
  123.             console.log(this.TAG, "getState isStartBlueTooth: " + this.isStartBlueTooth);
  124.           }
  125.         });
  126.       }
  127.     } catch (err) {
  128.       console.error(this.TAG,'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  129.     }
  130.   }
  131.   private isLog(){
  132.     console.log(this.TAG, "isLog isStartBlueTooth: " + this.isStartBlueTooth);
  133.     return true;
  134.   }
  135.   build() {
  136.     Column() {
  137.       if(this.userGrant){
  138.         if(this.isLog()){
  139.           Text("当前蓝牙设备信息:\n " + this.mCurrentDeviceName)
  140.             .fontSize(px2fp(80))
  141.             .margin({ top: px2vp(100) })
  142.             .fontWeight(FontWeight.Bold)
  143.           Text(this.isStartBlueTooth ? "蓝牙状态: 开启" : "蓝牙状态: 关闭")
  144.             .fontSize(px2fp(80))
  145.             .margin({ top: px2vp(100) })
  146.             .fontWeight(FontWeight.Bold)
  147.             .onClick(this.setBlueToothState)
  148.           Text(this.isStartScan ? "蓝牙扫描: 开启ing" : "蓝牙扫描: 关闭")
  149.             .margin({ top: px2vp(100) })
  150.             .fontSize(px2fp(80))
  151.             .fontWeight(FontWeight.Bold)
  152.             .onClick(this.setBlueToothScan)
  153.           this.ListView()
  154.         }
  155.       }
  156.     }
  157.     .justifyContent(FlexAlign.Center)
  158.     .height('100%')
  159.     .width('100%')
  160.   }
  161.   @Builder ListView(){
  162.     List() {
  163.       ForEach(this.mListDeviceInfo, (item: DeviceInfo, index: number) => {
  164.         ListItem() {
  165.           Column(){
  166.             Row() {
  167.               Text("设备ID: " + item.deviceId).fontSize(px2fp(42)).fontColor(Color.Black)
  168.               Blank()
  169.               Text("设备名: " + item.deviceName).fontSize(px2fp(42)).fontColor(Color.Black)
  170.             }
  171.             .width('100%')
  172.             Text(item.deviceClass).fontSize(px2fp(42)).fontColor(Color.Black)
  173.           }
  174.           .width('100%')
  175.           .height(px2vp(200))
  176.           .justifyContent(FlexAlign.Start)
  177.           .onClick(()=>{
  178.             // 点击选择处理配对
  179.             AlertDialog.show({
  180.               title:"选择配对",
  181.               message:"是否选择该设备进行蓝牙配对?",
  182.               autoCancel: true,
  183.               primaryButton: {
  184.                 value:"确定",
  185.                 action:()=>{
  186.                   promptAction.showToast({ message: item.deviceName + "配对ing!"});
  187.                   BlueToothMgr.Ins().pairDevice(item.deviceId);
  188.                 }
  189.               },
  190.               secondaryButton: {
  191.                 value:"取消",
  192.                 action:()=>{
  193.                   promptAction.showToast({ message: "取消!"});
  194.                 }
  195.               },
  196.               cancel:()=>{
  197.                 promptAction.showToast({ message: "取消!"});
  198.               }
  199.             })
  200.           })
  201.         }
  202.       }, (item: string, index: number) => JSON.stringify(item) + index)
  203.     }
  204.     .width('100%')
  205.   }
  206. }
复制代码
         蓝牙管理类
         
  1. import { access, ble } from '@kit.ConnectivityKit';
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. import { connection } from '@kit.ConnectivityKit';
  4. export class BlueToothMgr {
  5.   private TAG: string = "BlueToothTest";
  6.   private static mBlueToothMgr: BlueToothMgr | undefined = undefined;
  7.   private advHandle: number = 0xFF; // default invalid value
  8.   private mDeviceDiscoverArr: Array<string> = new Array<string>();
  9.   public static Ins(){
  10.     if(!BlueToothMgr.mBlueToothMgr){
  11.       BlueToothMgr.mBlueToothMgr = new BlueToothMgr();
  12.       BlueToothMgr.init();
  13.     }
  14.     return BlueToothMgr.mBlueToothMgr;
  15.   }
  16.   private static init(){
  17.     try {
  18.       connection.on('pinRequired', (data: connection.PinRequiredParam) =>{
  19.         // data为配对请求参数
  20.         console.info("BlueToothTest",'pinRequired pin required = '+ JSON.stringify(data));
  21.       });
  22.     } catch (err) {
  23.       console.error("BlueToothTest", 'pinRequired errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  24.     }
  25.   }
  26.   /**
  27.    * 当前设备蓝牙设备名称
  28.    */
  29.   public getCurrentDeviceName(){
  30.     let localName: string = "";
  31.     try {
  32.       localName = connection.getLocalName();
  33.       console.info(this.TAG, 'getCurrentDeviceName localName: ' + localName);
  34.     } catch (err) {
  35.       console.error(this.TAG, 'getCurrentDeviceName errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  36.     }
  37.     return localName;
  38.   }
  39.   /**
  40.    * 当前设备蓝牙可发现状态
  41.    */
  42.   public isCurrentDiscovering(){
  43.     let res: boolean = false;
  44.     try {
  45.       res = connection.isBluetoothDiscovering();
  46.       console.info(this.TAG, 'isCurrentDiscovering isBluetoothDiscovering: ' + res);
  47.     } catch (err) {
  48.       console.error(this.TAG, 'isCurrentDiscovering errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  49.     }
  50.     return res;
  51.   }
  52.   // STATE_OFF        0        表示蓝牙已关闭。
  53.   // STATE_TURNING_ON        1        表示蓝牙正在打开。
  54.   // STATE_ON        2        表示蓝牙已打开。
  55.   // STATE_TURNING_OFF        3        表示蓝牙正在关闭。
  56.   // STATE_BLE_TURNING_ON        4        表示蓝牙正在打开LE-only模式。
  57.   // STATE_BLE_ON        5        表示蓝牙正处于LE-only模式。
  58.   // STATE_BLE_TURNING_OFF        6        表示蓝牙正在关闭LE-only模式。
  59.   public getBlueToothState(): access.BluetoothState {
  60.     let state = access.getState();
  61.     return state;
  62.   }
  63.   /**
  64.    * 设置蓝牙访问(开关状态)
  65.    * @param isAccess true: 打开蓝牙
  66.    */
  67.   setBlueToothAccess(isAccess: boolean, callbackBluetoothState: Callback<access.BluetoothState>){
  68.     try {
  69.       if(isAccess){
  70.         console.info(this.TAG, 'bluetooth enableBluetooth 1');
  71.         access.enableBluetooth();
  72.         console.info(this.TAG, 'bluetooth enableBluetooth 2');
  73.         access.on('stateChange', (data: access.BluetoothState) => {
  74.           let btStateMessage = this.switchState(data);
  75.           if (btStateMessage == 'STATE_ON') {
  76.             access.off('stateChange');
  77.           }
  78.           console.info(this.TAG, 'bluetooth statues: ' + btStateMessage);
  79.           callbackBluetoothState(data);
  80.         })
  81.       }else{
  82.         console.info(this.TAG, 'bluetooth disableBluetooth 1');
  83.         access.disableBluetooth();
  84.         console.info(this.TAG, 'bluetooth disableBluetooth 2');
  85.         access.on('stateChange', (data: access.BluetoothState) => {
  86.           let btStateMessage = this.switchState(data);
  87.           if (btStateMessage == 'STATE_OFF') {
  88.             access.off('stateChange');
  89.           }
  90.           console.info(this.TAG, "bluetooth statues: " + btStateMessage);
  91.           callbackBluetoothState(data);
  92.         })
  93.       }
  94.     } catch (err) {
  95.       console.error(this.TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  96.     }
  97.   }
  98.   private switchState(data: access.BluetoothState){
  99.     let btStateMessage = '';
  100.     switch (data) {
  101.       case 0:
  102.         btStateMessage += 'STATE_OFF';
  103.         break;
  104.       case 1:
  105.         btStateMessage += 'STATE_TURNING_ON';
  106.         break;
  107.       case 2:
  108.         btStateMessage += 'STATE_ON';
  109.         break;
  110.       case 3:
  111.         btStateMessage += 'STATE_TURNING_OFF';
  112.         break;
  113.       case 4:
  114.         btStateMessage += 'STATE_BLE_TURNING_ON';
  115.         break;
  116.       case 5:
  117.         btStateMessage += 'STATE_BLE_ON';
  118.         break;
  119.       case 6:
  120.         btStateMessage += 'STATE_BLE_TURNING_OFF';
  121.         break;
  122.       default:
  123.         btStateMessage += 'unknown status';
  124.         break;
  125.     }
  126.     return btStateMessage;
  127.   }
  128.   /**
  129.    * 主播蓝牙广播
  130.    */
  131.   public registerBroadcast(){
  132.     try {
  133.       ble.on('advertisingStateChange', (data: ble.AdvertisingStateChangeInfo) => {
  134.         console.info(this.TAG, 'bluetooth advertising state = ' + JSON.stringify(data));
  135.         AppStorage.setOrCreate('advertiserState', data.state);
  136.       });
  137.     } catch (err) {
  138.       console.error(this.TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  139.     }
  140.   }
  141.   /**
  142.    * 开启蓝牙广播
  143.    */
  144.   public async startBroadcast(valueBuffer: Uint8Array){
  145.     // 表示发送广播的相关参数。
  146.     let setting: ble.AdvertiseSetting = {
  147.       // 表示广播间隔,最小值设置160个slot表示100ms,最大值设置16384个slot,默认值设置为1600个slot表示1s。
  148.       interval: 160,
  149.       // 表示发送功率,最小值设置-127,最大值设置1,默认值设置-7,单位dbm。推荐值:高档(1),中档(-7),低档(-15)。
  150.       txPower: 0,
  151.       // 表示是否是可连接广播,默认值设置为true,表示可连接,false表示不可连接。
  152.       connectable: true
  153.     };
  154.     // BLE广播数据包的内容。
  155.     let manufactureDataUnit: ble.ManufactureData = {
  156.       // 表示制造商的ID,由蓝牙SIG分配。
  157.       manufactureId: 4567,
  158.       manufactureValue: valueBuffer.buffer
  159.     };
  160.     let serviceValueBuffer = new Uint8Array(4);
  161.     serviceValueBuffer[0] = 5;
  162.     serviceValueBuffer[1] = 6;
  163.     serviceValueBuffer[2] = 7;
  164.     serviceValueBuffer[3] = 8;
  165.     // 广播包中服务数据内容。
  166.     let serviceDataUnit: ble.ServiceData = {
  167.       serviceUuid: "00001888-0000-1000-8000-00805f9b34fb",
  168.       serviceValue: serviceValueBuffer.buffer
  169.     };
  170.     // 表示广播的数据包内容。
  171.     let advData: ble.AdvertiseData = {
  172.       serviceUuids: ["00001888-0000-1000-8000-00805f9b34fb"],
  173.       manufactureData: [manufactureDataUnit],
  174.       serviceData: [serviceDataUnit],
  175.       includeDeviceName: false // 表示是否携带设备名,可选参数。注意带上设备名时广播包长度不能超出31个字节。
  176.     };
  177.     // 表示回复扫描请求的响应内容。
  178.     let advResponse: ble.AdvertiseData = {
  179.       serviceUuids: ["00001888-0000-1000-8000-00805f9b34fb"],
  180.       manufactureData: [manufactureDataUnit],
  181.       serviceData: [serviceDataUnit]
  182.     };
  183.     // 首次启动广播设置的参数。
  184.     let advertisingParams: ble.AdvertisingParams = {
  185.       advertisingSettings: setting,
  186.       advertisingData: advData,
  187.       advertisingResponse: advResponse,
  188.       //         表示发送广播持续的时间。单位为10ms,有效范围为1(10ms)到65535(655350ms),如果未指定此参数或者将其设置为0,则会连续发送广播。
  189.       duration: 0 // 可选参数,若大于0,则广播发送一段时间后,则会临时停止,可重新启动发送
  190.     }
  191.     // 首次启动广播,且获取所启动广播的标识ID
  192.     try {
  193.       this.registerBroadcast();
  194.       this.advHandle = await ble.startAdvertising(advertisingParams);
  195.     } catch (err) {
  196.       console.error(this.TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  197.     }
  198.   }
  199.   /**
  200.    * 开始蓝牙扫描
  201.    */
  202.   public startScanDevice(callback: Callback<Array<string>>){
  203.     try {
  204.       connection.on('bluetoothDeviceFind', (data: Array<string>)=>{
  205.         // 随机MAC地址
  206.         console.info(this.TAG, 'bluetooth device bluetoothDeviceFind = '+ JSON.stringify(data));
  207.         callback(data);
  208.       });
  209.       connection.startBluetoothDiscovery();
  210.     } catch (err) {
  211.       console.error(this.TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  212.     }
  213.   }
  214.   /**
  215.    * 停止蓝牙扫描
  216.    */
  217.   public stopScanDevice(){
  218.     try {
  219.       connection.off('bluetoothDeviceFind');
  220.       connection.stopBluetoothDiscovery();
  221.     } catch (err) {
  222.       console.error(this.TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  223.     }
  224.   }
  225.   public getDeviceName(deviceID: string){
  226.     let remoteDeviceName: string = "";
  227.     try {
  228.       remoteDeviceName = connection.getRemoteDeviceName(deviceID);
  229.       console.info(this.TAG, 'getDeviceName device = '+ JSON.stringify(remoteDeviceName));
  230.     } catch (err) {
  231.       console.error(this.TAG, 'getDeviceName errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  232.     }
  233.     return remoteDeviceName;
  234.   }
  235.   public getDeviceClass(deviceID: string){
  236.     let remoteDeviceClass: string = "";
  237.     try {
  238.       let classObj = connection.getRemoteDeviceClass(deviceID);
  239.       remoteDeviceClass = JSON.stringify(classObj);
  240.     } catch (err) {
  241.       console.error(this.TAG, 'getDeviceName errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  242.     }
  243.     return remoteDeviceClass;
  244.   }
  245.   // BondState
  246.   // BOND_STATE_INVALID        0        无效的配对。
  247.   // BOND_STATE_BONDING        1        正在配对。
  248.   // BOND_STATE_BONDED        2        已配对。
  249.   /**
  250.    * 发起配对蓝牙
  251.    */
  252.   public pairDevice(deviceID: string){
  253.     try {
  254.       connection.on('bondStateChange', (data: connection.BondStateParam) =>{
  255.         console.info(this.TAG, 'pairDevice pair state = '+ JSON.stringify(data));
  256.         // 当蓝牙配对类型PinType为PIN_TYPE_ENTER_PIN_CODE或PIN_TYPE_PIN_16_DIGITS时调用此接口,请求用户输入PIN码。
  257.       });
  258.       connection.pairDevice(deviceID, (err: BusinessError) => {
  259.         console.info(this.TAG, 'pairDevice device name err:' + JSON.stringify(err));
  260.       });
  261.     } catch (err) {
  262.       console.error(this.TAG, 'pairDevice errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  263.     }
  264.   }
  265. }
复制代码
         ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/fda00352f394401cbc3a6b884469f6ce.png![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/2208c37af700411ea0140f7e4141d59c.png)

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4