收藏 分销(赏)

用WinRunner实现软件的全球化测试.doc

上传人:仙人****88 文档编号:9408398 上传时间:2025-03-25 格式:DOC 页数:26 大小:579.50KB
下载 相关 举报
用WinRunner实现软件的全球化测试.doc_第1页
第1页 / 共26页
用WinRunner实现软件的全球化测试.doc_第2页
第2页 / 共26页
点击查看更多>>
资源描述
用WinRunner实现软件的全球化测试 摘要 本文采用循序渐进的方法详细的介绍了如何用WinRunner实现软件的全球化测试。当然,单靠WinRunner本身是无法完全实现的,我们开发了一个小程序COFAL来帮助WinRunner实现全球化测试。通过学习这篇文章,您可以掌握: ● WinRunner的在全球化测试中的缺陷 ● WinRunner本身可用于全球化测试的地方 ● COFAL如何帮助WinRunner实现全球化测试 ● COFAL的实现细节 关键字 Globalization(g11n),Internationalization(i18n),localization(l10n),Code Once Fit All Language(COFAL) 1. 背景 全球化已经成为当今软件发展的趋势,许多大型跨国软件公司都在亚洲设立了自己的专门从事全球化测试的部门。2004年的8月,我加入Oracle甲骨文北京研发中心,正式成为这其中的一员,我测试的软件是Oracle Application Server 10g,以下简称Oracle AS。Oracle AS是一个基于J2EE架构的应用系统,详细的介绍您可以参考OTN上的相关文档。 1.1 全球化中的概念 全球化的英文是Globalization,由于单词较长,所以为了书写方便,通常缩写为G11N,中间的11代表首字母”G”和尾字母”N”之间省略的11个字母。 引用”中国本地化”网站上对全球化的定义:Globalization是使产品或软件进入全球市场而进行的有关的商务活动。包括正确的国际化设计,本地化集成,以及在全球市场进行的市场推广、销售和支持的全部过程。 全球化中与我们测试直接相关的有国际化设计和本地化集成。 国际化的英文是internationlization,由于单词较长,通常缩写为I18N,中间的18代表首字母”I”和尾字母”N”之间省略的18个字母。引用”中国本地化”网站上的定义:国际化设计是指设计一个适用于多种语言和地区的应用程序的过程。适用于多种语言和地区的含义是当使用不同语言及处于不同的地区的用户在使用这个应用程序时,应用程序必须使用他们能看懂的语言和符合他们文化习惯来显示信息。 本地化的英文是localization,由于单词较长,通常缩写为L10N,中间的10代表首字母”L”和尾字母”N”之间生罗的10个字母。引用”中国本地化”网站上的定义:本地化是指将产品或软件针对特定国际语言和文化进行加工,使之符合特定区域市场的过程。真正的本地化要考虑目标区域市场的语言、文化、习俗和特性。通常包括改变软件的书写系统(输入法)、键盘使用、字体、日期、时间和货币格式等。 Locale表示表示一个特定的地理、政治或文化的区域,在java中有Locale类,我们会在1.3小节中给出详细的描述 1.2 全球化测试的内容 简单的说,全球化测试主要是测试软件的处理数据和显示数据的功能。以Oracle AS为例: ● 处理不同的字符集(encoding)数据 Oracle Internet Directory(简称OID)是一个LDAP服务器,数据保存在Oracle数据库中,现在想测试它创建用户的功能,要求用户的DN可以为不同国家的字符集,通俗的说,可以创建英文的DN,简体中文的DN和日文的DN等。当然具体可以创建哪些字符集的DN也要看当前Oracle数据库的字符集,只是那些在可以和当前字符集正确转换的字符集中的DN才可以正确的创建,否则很有可能无法创建或者创建的结果错误,如我们经常会看到的一些数据变成了问号(?)。 ● 动态显示与Locale有关的数据 Oracle Delegated Administration Services(简称DAS)是一个通过web页面访问的组件,页面的编码方式为UTF8,要求当选择不同的浏览器语言时,以下各项都可以显示为与当前Locale相符的形式: ☆ 页上元素的文本类型的属性 如某个页的标题,在英文下为“Home”,在中文下为“主页”;某个按钮上的标签,在英文下为“OK”,在中文下为“确定”。 ☆ 表示日期、时间、时区和货币等的文字 如某个页上的一段表示出生日期的文字,在英文下显示为“January 1, 1976”,在中文下显示为“1976年1月1日”。 1.3 Java程序的国际化设计 Java语言是平台无关的,它采用双字节字符编码(UTF16),在解决国际化问题上有天生的优势。下面我要介绍的是Java中“动态显示与Locale有关的数据”的原理。 这里要用到的几个主要类都在java.util包(package)中,包括有Locale、ResourceBundle、ListResourceBundle、PropertyResourceBundle等,其继承关系如下图所示: ● Locale 该类包含对主要地理区域的地域化特征的封装。通过设定Locale,我们可以为特定的国家或地区提供符合当地文化习惯的字体、符号、图标和表达格式。例如,我们可以通过获得特定Locale下的Calendar类的实例,显示符合特定表达格式的日期。Locale有以下三个构造函数: ☆ Locale(String language) ☆ Locale(String language,String country) ☆ Locale(String language,String country,String variant) language参数:代表两个小写英文字符的ISO语言编码,如zh表示Chinese,可用的语言编码可以参考: http://www.ics.uci.edu/pub/ietf/http/related/iso639.txt country参数:代表两个大写英文字符的ISO国家或地区编码,如,CN表示China,TW表示TAIWAN,国家代码对照表如下: http://userpage.chemie.fu-berlin.de/diverse/doc/ISO_3166.html variant参数:代表与供应商或浏览器相关的代码。如,WIN表示windows,MAC表示Macintosh,POSIX表示POSIX。当有两个variant存在的话,用下划线(uderscore)连接,并把最重要的variant放在前面。 下面是几个典型的Locale的例子 Locale("ja") Locale("zh","CN") Locale("zh","TW","WIN") Locale("es","ES""Traditional_WIN") Locale.getDefault(),得到当前Java虚拟机的宿主系统上默认的Locale ● ResourceBundle 该类是一个抽象类,它定义了三个静态方法来获得具体的实现类(ListResourceBundle的子类或PropertyResourceBundle类)的实例: ☆ static final ResourceBundle getBundle(String baseName) 等同于调用: getBundle(baseName,Locale.getDefault(),this.getClass.getClassLoader()) 使用的是系统缺省的Locale。 ☆ static final ResourceBundle getBundle(String baseName,Locale locale) 等同于调用: getBundle(baseName,locale,this.getClass.getClassLoader()) 使用的是参数locale指定的Locale。 ☆ static final ResourceBundle getBundle(String baseName,Locale locale,ClassLoader loader) 下面我们来说说baseName参数和locale参数。 BaseName参数指定的是一组ReourceBundle的公共的基础名称,例如,设baseName等于“TestBundle”。如果用ListResourceBundle子类来实现,则要有如下这样的类:TestBundle.class、 TestBundle_zh_CN.class和 TestBundle_fr.class等;如果用PropertyResourceBundle来实现,则要有如下这样的属性文件:TestBundle.properties、 TestBundle_zh_CN.properties和 TestBundle_fr.properties等。 locale参数和选择策略一起决定运行时具体选择这组ResourceBundle中的哪一个。 假设locale参数指定的Locale为(language1,country1,variant1),系统默认的Locale为(language2,country2,variant2),则按照以下优先级的顺序查找最满足条件的ResourceBundle: · baseName + "_" + language1 + "_" + country1 + "_" + variant1 · baseName + "_" + language1 + "_" + country1 · baseName + "_" + language1 · baseName + "_" + language2 + "_" + country2 + "_" + variant2 · baseName + "_" + language2 + "_" + country2 · baseName + "_" + language2 · baseName 在每一种情况下,会先尝试按ListResourceBundle类的方式加载,失败后会再尝试按照访问属性文件的方式加载PropertyResourceBundle类。如果所有这些情况都没有找到的话最后会抛出一个MissingResourceException的异常。 注意,在第一个getBundle静态函数中locale参数指定的Locale就是系统默认的Locale。 ● ListResourceBundle 该类继承ResourceBundle类,也是一个抽象类。它实现了ResourceBundle类中的抽象函数getKeys()和handleGetObject(String key),并提供了一个抽象函数getContents()。在应用中,通过创建继承ListResourceBundle的子类来实现ResourceBundle。要求子类必须实现getContents函数并提供一个包含有一组属性对的数组,如: package oracle.cdc.sgt.unicode; import java.util.ListResourceBundle; public class MResources extends ListResourceBundle { public Object[][] getContents() { return contents; } static final Object[][] contents = { {"s1", "Home"} }; } package oracle.cdc.sgt.unicode; import java.util.ListResourceBundle; public class MResources_zh_CN extends ListResourceBundle { public Object[][] getContents() { return contents; } static final Object[][] contents = { {"s1", "主页"} }; } 下面是一个java类根据不同的Locale从相应的ListResourceBundle子类中取数据来显示: package oracle.cdc.sgt.unicode; import java.util.ResourceBundle; import java.util.Locale; public class TestListResourceBundle { public static void main(String[] args) { ResourceBundle messages; Locale curloc; try { if (args.length != 2) { curloc = Locale.getDefault(); } else { curloc = new Locale(args[0], args[1]); } messages = ResourceBundle.getBundle( "oracle.cdc.sgt.unicode.MResources", curloc); System.out.println(messages.getString("s1")); } catch (Exception e) { System.out.println(e); } } } 把这三个类加入到classpath中,运行“java TestListResourceBundle zh CN”或在简体中文操作系统上运行“java TestListResourceBundle”,打印出“主页”;运行“java TestListResourceBundle en”或在英文操作系统上运行“java TestListResourceBundle”,打印出“Home”。 ● PropertyResourceBundle 继承ResourceBundle类,它不是抽象类,也不需要创建它的子类。与ListResourceBundle相同的是它也实现了ResourceBundle类的抽象函数getKeys()和handleGetObject(String key);不同的是,它是从属性文件(.properties)中读入属性对的。例如, 定义如下一组properties文件,并加入到classpath中: MResources.properties: s1=Home MResources_zh_CN.properties s1=主页 下面是一个java类根据不同的locale从相应的Properties文件中取数据来显示: package oracle.cdc.sgt.unicode; import java.util.Locale; import java.util.ResourceBundle; public class TestPropertyResourceBundle { public static void main(String[] args) { ResourceBundle messages; Locale curloc; if (args.length != 2) { curloc=Locale.getDefault(); } else { curloc = new Locale(args[0],args[1]); } messages = ResourceBundle.getBundle( "oracle/cdc/sgt/unicode/MResources", curloc); System.out.println(messages.getString("welcome")); } } 运行的方式和结果同TestListResourceBundle一样。 留意一下TestListResourceBunlde和TestPropertyResourceBundle唯一不同的地方就是在调用getBundle函数的那个语句,按照我们上面所说的,完全可以统一写成:“oracle.cdc.sgt.unicode.MResources”,因为getBundle会缺省先找基础名称为MRseources的类,失败后再找基础名称为MResources的属性文件,在查找属性文件前它会自动把“.”转换为“/”。当然,如果通过存在基础名称为MResources的类和属性文件时,也可以通过直接使用“oracle/cdc.sgt/unicode/MResources”来略过查找基础名称为MResources的类。 当然,java程序的国际化设计并不只是这么简单,当涉及日期和时间显示等问题时,还可以利用java.text包以及java.util包中的TimeZone、SimpleTimeZone和Calendar等类进行辅助处理。我们就不在这里详细叙述了,您只需要记住一个ResourceBundle的概念就可以了,本文的后续部分都是围绕着这个概念展开的。 2. WinRunner调研 WinRunner适合于测试那些有图形操作界面的组件。目前,我们手头可用的版本WinRunner7.5,启用Web和Java插件(plugin)。 让我们先从WinRunner的技术特点说起吧。 2.1 WinRunner的技术特征 由于本文不是专门介绍WinRunner的,所以只列举一些WinRunner的重要特征。注意:这里定义了一些非官方的术语,为的是便于您的理解。 ● WinRunner将对象(object)分为两种:窗口(window)和子对象,任一个子对象都隶属于一个窗口。 注意:窗口也是对象,如一个页面就是一个窗口。 ● WinRunner通过一组属性来唯一的识别窗口,也就是说不能有所有属性值都相同的多个窗口;同样的,WinRunner通过一组属性来唯一的识别同一个窗口下的子对象,也就是说,在同一个窗口下,不能有所有属性值都相同的多个子对象。 ● 如果把对象的所有属性的集合称为对象的定义,WinRunner可以把对象的定义保存在以下两个地方: ☆ 独立script的单独的扩展名为GUI的文件 简称为GUI文件,同时为每个object定义了一个绝对逻辑名,有了绝对逻辑名就一定有相对逻辑名。对于窗口来说,它的绝对逻辑名等于它的相对逻辑名;对于子对象,它的绝对逻辑名等于它隶属的窗口的绝对逻辑名后面加一个”.”再加上它的相对逻辑名。在script开始部分导入GUI文件,在后面部分中只需要写出对象的绝对逻辑名,就可以从GUI文件中获得这个对象的定义了。如: "OracleAS Certificate Authority-Certificate Management": { class: window, MSW_class: html_frame, html_name: "!OracleAS Certificate Authority-Certificate Mana.*" } { ltree_state: open, list_open_data: close } "OracleAS Certificate Authority-Certificate Management"."Advanced Search": { class: object, MSW_class: html_text_link, html_name: "Advanced Search" } "OracleAS Certificate Authority-Edit Policy Result: UniqueCertificateCo": { class: window, MSW_class: html_frame, html_name: "!OracleAS Certificate Authority-Edit Policy Resu.*" } { rtree_state: open, ltree_state: open, list_open_data: close } ☆ script本身 把object定义写在script是可以的。一种方法是象GUI文件那样在script的开始部分为对象定义一个绝对逻辑名,这样在script的后续部分就可以通过这个绝对逻辑名来访问对象的;另一种方法是不为对象定义绝对逻辑名,而是在每个要访问对象的地方,直接写该对象的定义。这两种在script中定义对象的方法我们都不推荐,第一种完全就是GUI文件在script中的实现,那么为什么不它放在GUI文件中统一管理呢,第二种虽然省略了导入GUI文件的一步,但是维护起来更麻烦了,如果对象的属性发生变化,那就要修改所有脚本中所有这样定义了该对象的地方。 也许我们还没有意识到在script中定义对象的好处,但是存在就是道理。如: #Gui Objects initialization set_window("{ class: window, MSW_class: html_frame, html_name: \"OracleAS Wireless Tools\"}",151); list_select_item("{ class: list, MSW_class: html_combobox, html_name: matchType}","Matches"); rc=global_web_obj_text_exists(access_info,"Wireless and Voice Settings"); if(text!="Application Links") set_window("{ class: window, MSW_class: html_frame, html_name: \"Oracle Enterprise Manager - Notification Event Collector: "&notification&"\"}",8); rc=global_web_obj_text_exists(text_object,"#1","#1","Access Information","",""); set_window("{ class: window, MSW_class: html_frame, html_name: \"OracleAS Wireless Tools\"}",151); 2.2 WinRunner在全球化测试中的局限性 在1.1“全球化测试的内容”一节中我们知道,要在不同的Locale下测试软件的处理数据和显示数据的能力。在不同Locale下,WinRunner赖以识别对象的属性列中有的属性也可能不同,因此在不同的Locale下,同一个对象的定义也可能不同。如同一个窗口,在英文下的html_name属性值为 “OracleAS Certificate Authority-Certificate Management”,在简体中文下的html_name属性值为 “OracleAS Certificate Authority-证书管理”。也就是如果用该窗口在英文下的定义是无法在简体中文或其他Locale下识别该窗口的。 换句话说,在一种Locale下录制的脚本,不论对象定义是保存在GUI文件中还是保存在script中,都无法直接拿到另一种Locale下直接运行。注意:所谓直接拿到,也包括进行少量的修改。 虽然可以在不同的Locale下用WinRunner录制各自的脚本,但是这并不是我们所希望的,那样做的成本是非常高的。 我们的目标是只在一种Locale下录制脚本,经过一定处理后,就可以在其他Locale下使用,即Code Once Fit All Language(简称COFAL)。 2.3 WinRunner满足COFAL的技术可行性 既然WinRunner是通过对象的定义(一组属性)来标识对象的,那么我们就要研究对象的属性在不同的Locale下有什么不同: ● 有的对象在不同的Locale下所有属性值都不变 ● 有的对象在不同的Locale下部分属性发生变化 只要找到变化的属性的规律和属性值的来源,并用自动化的方法来修改这些属性,就可以基本上满足只录制一次的需求。 1、如果对象的定义保存在GUI文件中 假如在英文下录制了一套脚本,该套脚本公用一个GUI文件global.gui,我们要找到一个自动化的方法,生成该GUI文件在其他Locale下对应的GUI文件,如global_zh_CN.gui和global_fr.gui等。这样,在不同Locale下,通过使用不同的GUI文件就可以用同一套脚本运行了。这样看来虽然有多个GUI文件,但是脚本只有一套,其他的GUI文件又是自动生成的,基本上满足了COFAL的要求。 2、如果对象的定义保存在script中 假如在英文下录制了一套脚本,脚本都保存在tina目录下,对象的定义都保存在script中。我们要找到一个自动化的方法,转化该script中对象的定义到不同的Locale下的定义,并把转化的结果保存在新的目录下,如tina_zh_CN和tina_fr目录等。这样,在不同的Locale下,通过使用不同目录下的脚本就可以了。这样看来虽然有多套脚本,但是只录制了一次,其他的都是自动生成的,也基本上满足了COFAL的要求。 下面我们会把转化对象定义统一称为“翻译”,接下来要介绍的就是以COFAL命名的一个实现自动化翻译的小工具。 3. 自动翻译工具COFAL简介 CORAL是Code Once Fit All Language的缩写,它是专门为配合WinRunner的全球化测试而开发一个工具,code once fit all language的意思是只需要在一种语言下编写脚本,就可以在所有语言下运行。用COFAL来实现script和GUI文件的自动翻译。 3.1 技术原理 3.1.1 Java应用程序级数据翻译 Oracle AS是一个基于J2EE架构的应用程序,它是通过我们在1.2节中介绍的java数据绑定机制来实现国际化的,也就是说那些需要翻译的属性值其实都是保存在ResourceBundle中。除了前面说过的ListResourceBundle和PropertyResourceBundle外,Oracle AS还把部分ResourceBundle保存在数据库的表中,在运行时根据不同的语言环境用绑定的key动态的从表中检索出对应的值。 由此可知,程序本身是通过关键字(key)结合指定的locale来获得值(value)的;而现在是想在已知值(value)和指定的locale的情况下,获得该key在其他locale下的值(value),这是一个逆向的过程。 为了描述方便,我们用key代表关键字,用prevalue代表当前可得的值,用postvalue代表翻译后的值。 自动翻译的原理是: (1) 事先定位好resourcebundle的保存位置。 (2) 翻译时,从script或GUI文件中提取出prevalue,然后用prevalue在ResourceBundle中查询出key,再用key从ResourceBundle中查询出postvalue。 (3) 用postvalue替换prevlaue,把替换的结果保存成新的语言版本。 在COFAL中,我们把Oracle AS用到的ResourceBundle统一保存在一张Oracle数据库表GLOBALRES中。翻译时通过JDBC,用prevalue从表中select出key,再用key去select出postvalue。 GLOBALRES表的结构如下: 列 类型 描述 Component varchar2(60) Key所属的组件 Version varchar2(60) 组件的版本 Prolang varchar2(60) 从ResourceBundle文件名中提取出来的表示语言的串,如从TestBundle_zh_CN.properties中提取出的zh_CN Key varchar2(400) 从ResourceBundle文件 内容中提取出来的(key-value)对中的key value nvarchar2(400) 从ResourceBundle文件 内容中提取出来的(key-value),因为有的value非常长,已经超出了4000个字节的字段长度限制,所以如果value超过4000个字节,提取vlaue中前400个字节否则保留全部字节。注意是400个字节不是4000个字节,因为在WinRunner中完全可以通过正则表达式的使用将字符串截短,不用保留那么长的串。 Value1 clob 这是一个备份字段,当value超过4000个字节时把整个value备份到这里,否则为空 location varchar2(1200) 保留(key-value)对的出处,即它来自于哪个ResourceBundle 注意:component和version字段是为了能更有效的翻译而引入的,在导入resourcebundle时应该确定它属于哪个component的哪个version,这样在翻译时就可以通过增加where判断条件来更具体化一个查询。 抛开如何获得prevalue先不说,最终要进行的select查询就是以下两种 ● SQL精确查询 ☆ 获得key的SQL语句 select key from GLOBALRES where value=’Add Another Row’ and prolang=’en’ and component=’UIX’ and version=’10.1.2.0.2’ ☆ 获得postvalue的SQL语句 假设上面的SQL语句查询出来的key是TABLE_ADD_ROW_SINGLE_TEXT,则获得postvalue的语句为: select value from GLOBALRES where key=’TABLE_ADD_ROW_SINGLE_TEXT’ and prolang=’zh_cn’ and component=’UIX’ and version=’10.1.2.0.2’ ● SQL模糊查询 ☆ 获得key的SQL语句 select key from GLOBALRES where value like 'Certificate Mana%') and prolang=’en’ and component=’oca’ and version=’10.1.2.0.2’ ☆ 获得postvalue的SQL语句 假设上面的SQL语句查询出来的key是OCAUIAdminTabText,则获得postvalue的语句为: select value from GLOBALRES where key=’OCAUIAdminTabText’ and prolang=’zh_cn’ and component=’oca’ and version=’10.1.2.0.2’ 看到这里,您也许会有一个疑问:从key到postvalue是一对一的关系,但是从prevalue到key则是多对一的关系,比如可能有多个key在英文下的value都是OK,这就是所谓的对象多翻译的情况,我们将在3.2.4小节说明如何处理这种情况。 3.1.2 操作系统级数据翻译 有时测试要打开一些操作系统级的窗口使用里面的一些对象,由于我们不知道如何获得它们的ResourceBundle,所以只好用一种最笨的办法: (1) 第一次将用到的操作系统级的对象手动搜集(可以使用WinRunner的Spy来录制)在一起,保存成在一个单独的resourcebundle中; (2) 以后再用到时,从这个resourcebundle里找。 在COFAL中,我们单独定义了一张表SYSRES来保存操作系统级的resourcebundle,表的结构如下: 字段名 字段类型 描述 win varchar2(120) 窗口的绝对逻辑名 obj varchar2(120) 对象的相对逻辑名,当对象是窗口是该字段那为null value nvarchar2(240) 对象的需要翻译的属性值 lang varchar2(10) 语言 这张表的结果完全是按照WinRunner中“窗口和子对象”的模式定义的,我们规定: ● win字段和obj字段都只能是英文字符串,取对象在英文环境下的名字。如“Security Alert”窗口中的“OK”按钮对应的win字段为“Security Alert”,ob字段为“OK”。 ● value字段实际上就是对象在不同语言环境下的名字。如“Security Alert”窗口在英文下的value是“Security Alert”,在简体中文下的value是“安全警报”。 由于我们作出了这样的规定,操作系统级数据只能从英文翻译到其他语言,且prevalue等于key,这样就可以用key直接查找postvalue,省略了从prevalue到key的过程。 另外,在SYSRES中没有象GLOBALRES中的component和version字段,这是因为操作系统的版本比较固定,不象产品那样会有很多个release,同时获得component对我们来说意义也不大。当然,您也可以根据您的实际情况添加这两个或其他字段。 3.2 翻译策略 需要翻译的prevalue来自于对象的属性值,但并不是所有的对象都需要翻译,即使需要翻译也不是所有的属性都需要翻译,即使找到了需要翻译的属性,它的值也一定都可以直接翻译,可能要经过一些特殊处理才能够翻译。下面我们就来一一说明。 3.2.1 通过class属性来判断object是否需要翻译 每个object的定义里都肯定要包含它的class属性,通过class属性可以判断出该object是否需要翻译。 3.2.1.1 不需要翻译的class 以下class类型一般不需要翻译: ● edit ● combobox ● check_button ● list ● radio_button 3.2.1.2 需要翻译的object 除了以上那些不需要翻译的class外,其他的class类型基本上都需要翻译。 注意,以上我们所说的class都是WinRunner能够理解的标准的web class,如果您自定义了一些新的class类型,是否需要翻译要由实际情况而定。 在Oracle AS中我们没有定义新的class类,一些非标准的object也可以用标准的class类型定义,如Oracle AS中的某一按钮的定义如下: "OracleAS Certificate Authority-Advanced Search".Go: { MSW_class: html_rect, class: object, html_name: "Go", location: 0 } 建议
展开阅读全文

开通  VIP会员、SVIP会员  优惠大
下载10份以上建议开通VIP会员
下载20份以上建议开通SVIP会员


开通VIP      成为共赢上传
相似文档                                   自信AI助手自信AI助手

当前位置:首页 > 教育专区 > 小学其他

移动网页_全站_页脚广告1

关于我们      便捷服务       自信AI       AI导航        抽奖活动

©2010-2025 宁波自信网络信息技术有限公司  版权所有

客服电话:4009-655-100  投诉/维权电话:18658249818

gongan.png浙公网安备33021202000488号   

icp.png浙ICP备2021020529号-1  |  浙B2-20240490  

关注我们 :微信公众号    抖音    微博    LOFTER 

客服