网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以戳这里获取
一个人可以走的很快,但一群人才气走的更远!岂论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术互换、学习资源、职场吐槽、大厂内推、口试辅导),让我们一起学习成长!
- 如果 Service 正在运行,则系统会直接回调 Service 的 onCommand()方法来启动 Service。这个场景需要先返回到设备主页面,然后再打开这个应用,首先返回主页面,点击右边的圆形按钮
设备主页,这时候Service在配景运行,然后再点一下圆形按钮,进入到应用页面。
这里是应用页面,目前只有一个新增的应用,其他两个是系统应用,这里是一个列表,你可以通过鼠标按住左键上下进行拖动。然后点击这个HelloWorld。
回到应用的主页面。这个时候你看日记
系统直接回调 Service 的 onCommand()方法来启动 Service。这样实际操纵一下是不是印象更深刻呢?为了使这个操纵更加易懂,我决定安装一个电脑录屏软件,然后再把录得视频转GIF,再贴到文章里,这样看起来就更加的易懂了。刚才说了启动,那么下面说制止。
③ 制止Service Ability
Service 一旦创建就会一直保持在配景运行,除非必须接纳内存资源,否则系统不会制止或烧毁 Service。开发者可以在 Service 中通过 terminateAbility()制止本 Service 或在其他 Ability调用 stopAbility()来制止 Service。
制止 Service 同样支持制止当地设备 Service 和制止远程设备 Service,使用方法与启动Service 一样。一旦调用制止 Service 的方法,系统便会尽快烧毁 Service。
有两种制止Service的方法,在Page Ability中制止,和在本Service中制止,先试一下第一种。
下面我们在MainAbilitySlice中增加一个制止服务的方法。
/**
- 制止当地服务 在Page Ability中制止Service
*/
private void stopLocalService() {
Intent intent = new Intent();
//构建操纵方式
Operation operation = new Intent.OperationBuilder()
// 设备id
.withDeviceId(“”)
// 应用的包名
.withBundleName(“com.llw.helloworld”)
// 跳转目标的路径名 通常是包名+类名
.withAbilityName(“com.llw.helloworld.ServiceAbility”)
.build();
//设置操纵
intent.setOperation(operation);
//制止服务
stopAbility(intent);
}
然后再点击按钮的时候调用。
然后先运行一下进入到主页面,然后点击Next按钮,看下面的日记。
可以看到当我们从其他的Page Ability中制止Service时,会先回调onBackground。因为这个时候服务是在前台运行的,系统会把服务放到配景,然后再通过stop来制止这个服务。
下面再看看在本Service中制止这个服务。可以通过一个延时服务来操纵,下面来看看代码怎么写的。
/**
*/
final static ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
private void stopService() {
// 延时任务
service.schedule(threadFactory.newThread(new Runnable() {
@Override
public void run() {
//制止服务当前服务
terminateAbility();
}
//延时三秒执行
}), 3, TimeUnit.SECONDS);
}
/**
*/
private ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(final Runnable r) {
return new Thread() {
@Override
public void run() {
r.run();
}
};
}
};
为什么要这么写呢?因为DS里面推荐使用ScheduledExecutorService ,否则我就直接用Timer大概Thread就可以了。创建了一个线程池,然后创建一个线程工厂,在进行延时操纵的时候,传入了三个参数,一个是线程工厂,里面有一个Runnable(),第二个参数代表数量,第三个参数是单位,上面的代码就是3秒。
下面直接运行到模拟器,然后等待三秒就会自动调用terminateAbility();制止Service。你会发现和通过其他的Page Ability制止服务的执行流程是一样的。
③ 连接Service Ability
如果 Service 需要与 Page Ability 或其他应用的 Service Ability 进行交互,则应创建用于连接的 Connection。Service 支持其他 Ability 通过 connectAbility()方法与其进行连接。
在使用 connectAbility()处理惩罚回调时,需要传入目标 Service 的 Intent 与 IAbilityConnection的实例。IAbilityConnection 提供了两个方法供开发者实现:onAbilityConnectDone() 用来处理惩罚连接的回调,onAbilityDisconnectDone() 用来处理惩罚断开连接的回调。
在MainAbilitySlice中添加如下代码:
/**
*/
private void connectService(){
// 连接 Service
Intent intent = new Intent();
//构建操纵方式
Operation operation = new Intent.OperationBuilder()
// 设备id
.withDeviceId(“”)
// 应用的包名
.withBundleName(“com.llw.helloworld”)
// 跳转目标的路径名 通常是包名+类名
.withAbilityName(“com.llw.helloworld.ServiceAbility”)
.build();
//设置操纵
intent.setOperation(operation);
//连接到服务
connectAbility(intent,connection);
}
// 创建连接回调实例
private IAbilityConnection connection = new IAbilityConnection() {
// 连接到 Service 的回调
@Override
public void onAbilityConnectDone(ElementName elementName, IRemoteObject iRemoteObject, int i) {
// 在这里开发者可以拿到服务端传过来 IRemoteObject 对象,从中剖析出服务端传过来的信息
}
// 断开与连接的回调
@Override
public void onAbilityDisconnectDone(ElementName elementName, int i) {
}
};
然后在点击的时候调用
别Service的onConnect方法中加入日记打印
下面运行一下:
连接乐成。
④ 断开Service Ability
断开服务其实就比力的简单了,调用**disconnectAbility()**方法即可,而且不消传intent,但是要传IAbilityConnection进入,以是可以可以这样来测试,在连接到Service之后马上断开连接。
//断开服务
disconnectAbility(connection);
然后运行起来,进入应用页面,然后点击Next。
OK,到这一步,信任你已经会基本操纵了。而Service的生命周期根据调用方法的差别,其生命周期有以下两种路径:
- 启动 Service 该 Service 在其他 Ability 调用 startAbility()时创建,然后保持运行。其他 Ability 通过调用stopAbility()来制止 Service,Service 制止后,系统会将其烧毁。
- 连接 Service 该 Service 在其他 Ability 调用 connectAbility()时创建,客户端可通过调用disconnectAbility()断开连接。多个客户端可以绑定到相同 Service,而且当全部绑定全部取消后,系统即会烧毁该 Service。
看一下官网的图片
⑤ 前台Service
刚才我们说的都是配景的Service,那么怎么到前台来呢?最通用的前台服务就是音乐播放了,用手机的时候它会在关照栏创建,然后播放音乐,那么在鸿蒙中需要怎么使用前台服务呢?使用前台 Service 并不复杂,开发者只需在 Service 创建的方法里,调用keepBackgroundRunning()将 Service 与关照绑定。调用 keepBackgroundRunning()方法前需要在配置文件中声明 ohos.permission.KEEP_BACKGROUND_RUNNING 权限,该权限是 normal 级别,同时还需要在配置文件中添加对应的 backgroundModes 参数。在onStop()方法中调用 cancelBackgroundRunning()方法可制止前台 Service。
说这么多没啥用,下面来实际操纵一下:
在connectService方法中注释断开服务
然后进入到ServiceAbility中,新一个启动前台服务的方法。
/**
*/
private void startupForegroundService(){
//创建关照哀求 设置关照id为9527
NotificationRequest request = new NotificationRequest(1005);
//创建普通关照
NotificationRequest.NotificationNormalContent content =
new NotificationRequest.NotificationNormalContent();
//设置关照的标题和内容
content.setTitle(“Title”).setText(“Text”);
//创建关照内容
NotificationRequest.NotificationContent notificationContent = new
NotificationRequest.NotificationContent(content);
//设置关照
request.setContent(notificationContent);
keepBackgroundRunning(1005,request);
HiLog.error(LABEL_LOG, “ServiceAbility::startupForegroundService”);
}
然后在onStart中调用。
别忘了在config.json中给相关的代码配置:
然后直接运行到主页面,之后会先启动Service,然后将Service变成前台服务。运行之后如下:
说真话目前也就只是日记打印出来了,但是我也不知道当前这个服务是不是在前台。
然后在onCommand中取消前台服务:
@Override
public void onCommand(Intent intent, boolean restart, int startId) {
HiLog.error(LABEL_LOG, “ServiceAbility: nCommand”);
cancelBackgroundRunning();
HiLog.error(LABEL_LOG, “ServiceAbility::cancelBackgroundRunning”);
}
再运行一次。
四、Data Ability讲解
使用 Data 模板的 Ability(以下简称“Data”)有助于应用管理其自身和其他应用存储数据的访问,并提供与其他应用共享数据的方法。Data 既可用于同设备差别应用的数据共享,也支持跨设备差别应用的数据共享。
数据的存放情势多样,可以是数据库,也可以是磁盘上的文件。Data 对外提供对数据的增、删、改、查,以及打开文件等接口,这些接口的具体实现由开发者提供。说起来和Android的ContentProvider有些像。
① URI 介绍
Data 的提供方和使用方都通过 URI(Uniform Resource Identifier)来标识一个具体的数据,比方数据库中的某个表或磁盘上的某个文件。HarmonyOS 的 URI 仍基于 URI 通用标准,格式如下:
- scheme:协议方案名,固定为“dataability”,代表 Data Ability 所使用的协议范例。
- authority:设备 ID,如果为跨设备场景,则为目的设备的 IP 地点;如果为当地设备场景,则不需要填写。
- path:资源的路径信息,代表特定资源的位置信息。
- query:查询参数。
- fragment:可以用于指示要访问的子资源。
URI 示例:
- 跨设备场景:dataability://device_id/com.huawei.dataability.persondata/person/10
- 当地设备:dataability:///com.huawei.dataability.persondata/person/10
② 访问 Data和声明使用权限
开发者可以通过 DataAbilityHelper 类来访问当前应用或其他应用提供的共享数据。
DataAbilityHelper 作为客户端,与提供方的 Data 进行通信。Data 接收到哀求后,执行相应的处理惩罚,并返回效果。DataAbilityHelper 提供了一系列与 Data Ability 对应的方法。
如果待访问的 Data 声明确访问需要权限,则访问此 Data 需要在配置文件中声明需要此权限。比如
reqPermissions 表示应用运行时向系统申请的权限。
说了这么多照旧来创建一个Data Ability吧,鼠标右键包名 → New → Ability → Empty Data Ability
这个的Visible和Service的Visible是同样的意思,勾选上就是运行其他应用步伐访问数据。
然后打开config.json,看创建DataAbility时,自动生成了那些代码。
可以看到type为“data”,另外还自带一个提供给外部数据的权限,已经访问这个DataAbility的uri。
然后看一下DataAbility的代码:
package com.llw.helloworld;
import ohos.aafwk.ability.Ability;
import ohos.aafwk.content.Intent;
import ohos.data.resultset.ResultSet;
import ohos.data.rdb.ValuesBucket;
import ohos.data.dataability.DataAbilityPredicates;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.utils.net.Uri;
import ohos.utils.PacMap;
import java.io.FileDescriptor;
public class DataAbility extends Ability {
private static final HiLogLabel LABEL_LOG = new HiLogLabel(3, 0xD001100, “Demo”);
@Override
public void onStart(Intent intent) {
super.onStart(intent);
HiLog.info(LABEL_LOG, “ProviderAbility onStart”);
}
@Override
public ResultSet query(Uri uri, String[] columns, DataAbilityPredicates predicates) {
return null;
}
@Override
public int insert(Uri uri, ValuesBucket value) {
HiLog.info(LABEL_LOG, “ProviderAbility insert”);
return 999;
}
@Override
public int delete(Uri uri, DataAbilityPredicates predicates) {
return 0;
}
@Override
public int update(Uri uri, ValuesBucket value, DataAbilityPredicates predicates) {
return 0;
}
@Override
public FileDescriptor openFile(Uri uri, String mode) {
return null;
}
@Override
public String[] getFileTypes(Uri uri, String mimeTypeFilter) {
return new String[0];
}
@Override
public PacMap call(String method, String arg, PacMap extras) {
return null;
}
@Override
public String getType(Uri uri) {
return null;
}
}
在创建的时候就生成了一些代码,基本的增删改查、打开文件、获取URI范例、获取文件范例、还有一个回调。再加上一个onStart方法,总共是9个,乍一看比力多。下面先来介绍 DataAbilityHelper 具体的使用步调。
创建 DataAbilityHelper
DataAbilityHelper 为开发者提供了 creator()方法来创建 DataAbilityHelper 实例。该方法为静态方法,有多个重载。最常见的方法是通过传入一个 context 对象来创建DataAbilityHelper 对象。
DataAbilityHelper 为开发者提供了一系列的接口来访问差别范例的数据(文件、数据库等)。
DataAbilityHelper 为开发者提供了 FileDescriptor openFile(Uri uri, String mode)方法来操纵文件。此方法需要传入两个参数,其中 uri 用来确定目标资源路径,mode 用来指定打开文件的方式,可选方式包含“r”(读), “w”(写), “rw”(读写),“wt”(覆盖写),“wa”(追加写),“rwt”(覆盖写且可读)。该方法返回一个目标文件的 FD(文件描述符),把文件描述符封装成流,开发者就可以对文件流进行自界说处理惩罚。比如:
// 读取文件描述符
try {
//通过文件描述符 读取指定uri的文件 ,“r”(读), “w”(写), “rw”(读写),“wt”(覆盖写),“wa”(追加写),“rwt”(覆盖写且可读)
FileDescriptor fileDescriptor = helper.openFile(Uri.parse(“dataability://com.llw.helloworld.DataAbility”),“r”);
//获取文件输入流
FileInputStream fileInputStream = new FileInputStream(fileDescriptor);
} catch (DataAbilityRemoteException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
DataAbilityHelper 为开发者提供了增、删、改、查以及批量处理惩罚等方法来操纵数据库。
下面代码来说明一下:
- query 查询方法,其中 uri 为目标资源路径,columns 为想要查询的字段。开发者的查询条件可以通过 DataAbilityPredicates 来构建。查询用户表中 id 在 1-10 之间的用户的年事,并把效果打印出来,代码示比方下:
/**
*/
private void queryData(DataAbilityHelper helper) {
//构建uri
Uri uri = Uri.parse(“dataability://com.llw.helloworld.DataAbility”);
//构建查询字段
String[] column = {“age”};
// 构造查询条件
DataAbilityPredicates predicates = new DataAbilityPredicates();
//查询用户id在1~10之间的数据
predicates.between(“userId”,1,10);
//进行查询
try {
//用一个效果集来接收查询返回的数据
ResultSet resultSet = helper.query(uri,column,predicates);
//从第一行开始
resultSet.goToFirstRow();
//处理惩罚每一行的数据
do {
// 在此处理惩罚 ResultSet 中的记录
HiLog.info(LABEL_LOG, resultSet.toString());
}while (resultSet.goToNextRow());
} catch (DataAbilityRemoteException e) {
e.printStackTrace();
}
}
- insert 插入方法,其中 uri 为目标资源路径,ValuesBucket 为要新增的对象。插入一条用户信息的代码示比方下:
/**
*/
private void insertData(DataAbilityHelper helper) {
//构建uri
Uri uri = Uri.parse(“dataability://com.llw.helloworld.DataAbility”);
// 构造插入数据
ValuesBucket valuesBucket = new ValuesBucket();
valuesBucket.putString(“name”,“KaCo”);
valuesBucket.putInteger(“age”,24);
try {
helper.insert(uri,valuesBucket);
} catch (DataAbilityRemoteException e) {
e.printStackTrace();
}
}
- batchInsert 批量插入方法,和 insert()雷同。批量插入用户信息的代码示比方下:
/**
- 插入 多条数据
- @param helper 数据帮助类
*/
private void batchInsertData(DataAbilityHelper helper) {
//构建uri
Uri uri = Uri.parse(“dataability://com.llw.helloworld.DataAbility”);
// 构造插入数据
ValuesBucket[] valuesBuckets = new ValuesBucket[3];
//构建第一条数据
valuesBuckets[0] = new ValuesBucket();
valuesBuckets[0].putString(“name”,“Jim”);
valuesBuckets[0].putInteger(“age”,18);
//构建第二条数据
valuesBuckets[1] = new ValuesBucket();
valuesBuckets[1].putString(“name”,“Tom”);
valuesBuckets[1].putInteger(“age”,20);
//构建第三条数据
valuesBuckets[2] = new ValuesBucket();
valuesBuckets[2].putString(“name”,“Kerry”);
valuesBuckets[2].putInteger(“age”,24);
try {
//批量插入数据
helper.batchInsert(uri,valuesBuckets);
} catch (DataAbilityRemoteException e) {
e.printStackTrace();
}
}
- delete 删除方法,其中删除条件可以通过 DataAbilityPredicates 来构建。删除用户表中 id 在 1-10 之间的用户,代码示比方下:
/**
*/
private void deleteData(DataAbilityHelper helper) {
//构建uri
Uri uri = Uri.parse(“dataability://com.llw.helloworld.DataAbility”);
// 构造删除条件
DataAbilityPredicates predicates = new DataAbilityPredicates();
//用户id在1~10的数据
predicates.between(“userId”,1,10);
try {
//删除
helper.delete(uri,predicates);
} catch (DataAbilityRemoteException e) {
e.printStackTrace();
}
}
- update 更新方法,更新数据由 ValuesBucket 传入,更新条件由 DataAbilityPredicates 来构建。更新 id 为 2 的用户,代码示比方下:
/**
*/
private void updateData(DataAbilityHelper helper) {
//构造uri
Uri uri = Uri.parse(“dataability://com.llw.helloworld.DataAbility”);
//构造更新数据
ValuesBucket valuesBucket = new ValuesBucket();
valuesBucket.putString(“name”,“Aoe”);
valuesBucket.putInteger(“age”,66);
//构造更新条件
DataAbilityPredicates predicates = new DataAbilityPredicates();
//userId为2的用户
predicates.equalTo(“userId”,2);
try {
//更新数据
helper.update(uri,valuesBucket,predicates);
} catch (DataAbilityRemoteException e) {
e.printStackTrace();
}
}
- executeBatch 此方法用来执行批量操纵。DataAbilityOperation 中提供了设置操纵范例、数据和操纵条件的方法,开发者可自行设置本身要执行的数据库操纵。插入多条数据的代码示比方下:
/**
- 批量操纵数据
- @param helper 数据帮助类
*/
private void executeBatchData(DataAbilityHelper helper) {
//构造uri
Uri uri = Uri.parse(“dataability://com.llw.helloworld.DataAbility”);
//构造批量操纵
//第一个
ValuesBucket valuesBucket1 = new ValuesBucket();
valuesBucket1.putString(“name”,“Karen”);
valuesBucket1.putInteger(“age”,24);
//构建批量插入
DataAbilityOperation operation1 = DataAbilityOperation.newInsertBuilder(uri).withValuesBucket(valuesBucket1).build();
//第二个
ValuesBucket valuesBucket2 = new ValuesBucket();
valuesBucket2.putString(“name”,“Leo”);
valuesBucket2.putInteger(“age”,48);
DataAbilityOperation operation2 = DataAbilityOperation.newInsertBuilder(uri).withValuesBucket(valuesBucket2).build();
ArrayList operations = new ArrayList<>();
operations.add(operation1);
operations.add(operation2);
try {
//获取批量操纵数据的效果
DataAbilityResult[] results = helper.executeBatch(uri,operations);
HiLog.debug(LABEL_LOG,results.length+“”);
} catch (DataAbilityRemoteException e) {
e.printStackTrace();
} catch (OperationExecuteException e) {
e.printStackTrace();
}
}
③ 创建Data
确定命据存储方式
确定命据的存储方式,Data 支持以下两种数据情势:
- 文件数据:如文本、图片、音乐等。
- 布局化数据:如数据库等。
下面创建一个UserDataAbility,注意勾选上Visible
实现 UserDataAbility
UserDataAbility 接收其他应用发送的哀求,提供外部步伐访问的入口,从而实现应用间的数据访问。Data 提供了文件存储和数据库存储两组接口供用户使用。
文件存储
开发者需要在 Data 中重写 FileDescriptor openFile(Uri uri, String mode)方法来操纵文件:uri 为客户端传入的哀求目标路径;mode 为开发者对文件的操纵选项,可选方式包含“r”(读), “w”(写), “rw”(读写)等。
MessageParcel 类提供了一个静态方法,用于获取 MessageParcel 实例。通过dupFileDescriptor()函数复制待操纵文件流的文件描述符,并将其返回,供远端应用使用。示例,根据传入uri打开对应的文件,在UserDataAbility中写入如下方法
/**
*/
private void openUriFile() {
//构建uri
Uri uri = Uri.parse(“dataability://com.llw.helloworld.UserDataAbility”);
//获取文件 通过uri获取解码路径列表的第2条数据
File file = new File(uri.getDecodedPathList().get(1));
//只读
file.setReadOnly();
try {
//文件输入流
FileInputStream fileInputStream = new FileInputStream(file);
//得到文件描述符
FileDescriptor fileDescriptor = fileInputStream.getFD();
//绑定文件描述符
MessageParcel.dupFileDescriptor(fileDescriptor);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
数据库存储
初始化数据库连接。系统会在应用启动时调用 onStart()方法创建 Data 实例。在此方法中,开发者应该创建数据库连接,并获取连接对象,以便后续和数据库进行操纵。为了避免影相应用启动速率,开发者应当尽大概将非须要的耗时任务推迟到使用时执行,而不是在此方法中执行全部初始化。示例:
首先要创建一个数据实体bean
package com.llw.helloworld;
import ohos.data.orm.OrmObject;
public class BookStore extends OrmObject {
private int id;
private String bookName;
private double price;
private int page;
private String author;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
然后在UserDataAbility中如下:
上面的代码是官方文档里面的,可以看到这里是有一个地方报错的,因为少了一个参数,然后看一下getOrmContext方法少什么参数。
然后来看一下OrmMigration的源码
这是一个抽象类,可以通过继续的方式去实现它里面的方法。
下面我创建一个TestOrmContext1继续OrmMigration,里面的代码如下:
package com.llw.helloworld;
import ohos.data.orm.OrmMigration;
import ohos.data.rdb.RdbStore;
public class TestOrmContext1 extends OrmMigration {
/**
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以戳这里获取
一个人可以走的很快,但一群人才气走的更远!岂论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术互换、学习资源、职场吐槽、大厂内推、口试辅导),让我们一起学习成长!
hor;
}
public void setAuthor(String author) {
this.author = author;
}
}
然后在UserDataAbility中如下:
上面的代码是官方文档里面的,可以看到这里是有一个地方报错的,因为少了一个参数,然后看一下getOrmContext方法少什么参数。
然后来看一下OrmMigration的源码
这是一个抽象类,可以通过继续的方式去实现它里面的方法。
下面我创建一个TestOrmContext1继续OrmMigration,里面的代码如下:
package com.llw.helloworld;
import ohos.data.orm.OrmMigration;
import ohos.data.rdb.RdbStore;
public class TestOrmContext1 extends OrmMigration {
/**
[外链图片转存中…(img-mKCYwRCg-1715794760133)]
[外链图片转存中…(img-4xbww7Hw-1715794760134)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以戳这里获取
一个人可以走的很快,但一群人才气走的更远!岂论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术互换、学习资源、职场吐槽、大厂内推、口试辅导),让我们一起学习成长!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |