




已阅读5页,还剩67页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
SOA Framework 开发使用手册1 Configuration1.1 模块概述在公共组件库中的MCS.Library.Configuration 命名空间下包含了框架中实现应用程序配置的读取和写入的功能模块,该模块有如下特点:l 允许一台服务器上多个应用共享一个全局配置信息l 允许一台服务器上Web和Windows应用共享一个全局配置信息l 允许多台服务器上的应用共享一个全局配置信息l 充分利用了公共组件库的Cache管理1.2 .NET中的Configuration.NET通过XML格式的文件Machine.Config和Web.Config/App.Config来完成对应用程序的配置。整个服务器的配置信息保存在Machine.Config文件中,它包含了运行所有.NET程序,服务器需要的所有配置信息。Web.Config/App.Config从Machine.Config继承和重写部分配置信息。我们来看一下Machine.Config中定义的appSettings,type属性说明在.NET中以System.Configuration.AppSettingsSection这个类型来描述它。 再来看一下我们常见的Web.Config中appSettings这个Section的结构,它从Machine.Config中继承并重写了appSettings 在. NET中我们使用ConfigurationManager来获取ConfigurationSection,ConfigurationSection是配置信息中的配置节和内存中的对象的一个映射,通过ConfigurationSection可以去定义和管理一组相关的配置信息。ConfigurationSection appSetting = ConfigurationManager.GetSection(appSettings) as ConfigurationSection;我们可以为每一个应用程序配置单独的配置文件,如果有些配置信息是多个应用程序共享的,那么我们可以把这些配置信息放到Machine.config中,因为应用程序的配置信息继承自Machine.Config,这样就可以解决我们的问题了。但是这样做也同样存在问题,Machine.config是属于Server级别的,如果修改了它,可能会影响到所有寄宿在这台服务器上的.NET应用程序,可以即共享了配置,又不影响其他的应用程序吗?1.3 框架中的Configuration在框架中我们使用一个映射文件和一个共享全局配置文件来解决共享配置的问题。1.3.1 映射文件在. NET中我们使用ConfigurationManager来获取ConfigurationSection来获取配置信息,在框架中我们只是换种写法,用ConfigurationBroker来获取配置信息。ConfigurationBroker.GetSection(appSettings );那么如何可以做到让多个应用程序共享配置信息呢,首先我们修改一下Web.config中AppSetting配置节的信息。添加一个指向物理文件的信息 ConfigMapping.config这个文件究竟是做什么用的呢?我们先来看看它的结构ConfigMapping中定义了sourceMappings,它以框架中的MetaConfigurationSource-InstanceSection这个类型来描述。sourceMappings中描述了共享配置文件的信息,与appSettings一样,指向了共享配置文件的物理路径,要注意Mode这个属性。l name:全局配置文件的标识名称,此名称不可重复l Path:全局配置文件路径,可以是绝对路径或相对路径l mode:调用全局配置文件的程序的宿主类型,有web,win两种方式,框架在加载配置时只选择mode为win或web的全局配置文件。如果一个Web程序和一个Win程序共享配置信息,只需要添加一个配置元素即可。1.3.2 全局配置文件这样我们就可以为应用程序添加共享配置信息了。共享配置文件也是以Section组成的。 对应的访问类TestSection,自定义的Section要从system. Configuration. ConfigurationSection派生public class TestSection:ConfigurationSectionConfigurationProperty(country)public string Countrygetreturn thiscountry.ToString(); ConfigurationProperty(subTestSettingCollection)public SubTestSettingCollection SubTestSettingsgetreturn (SubTestSettingCollection)thissubTestSettingCollection;TestSection中一般会存放一组相关的配置信息,通常是一个集合,这种集合我们也需要一个类来描述它,在这里我们定义了 SubTestSettingCollection来描述这个集合,它派生自system. Configuration. ConfigurationElementCollection。public class SubTestSettingCollection : ConfigurationElementCollection/ 重写基类抽象方法,以CityName获取配置元素protected override object GetElementKey(ConfigurationElement element)return (SubTestSetting)element).CityName;集合中的元素就是我们想要的配置信息了,要读取这些信息.NET提供了ConfigurationElement这个基类,只要派生这个类,就可以很方便的读取信息。配置文件写法 相对应的映射类public sealed class SubTestSetting : ConfigurationElementConfigurationProperty(cityname)public string CityNamegetreturn (string)thiscityname;1.3.3 调用方式在框架中对所有的Section的访问都通过ConfigurationBroker来进行。 TestSection section = ConfigurationBroker.GetSection(testSetting) as TestSection;string country = section.Country;SubTestSettingCollection collection = section.SubTestSettings;foreach (SubTestSetting subtest in collection)string str = subtest.CityName;1.4 实现原理当首次向框架提出一个获得Section的请求后,获取web.Config/App.config和machine.config,查找Appsetting节点中的MCS.MetaConfiguration 元素的Mapping.config路径。如果有则读取Mapping.config文件,根据Mapping文件的Framework.config路径读取Framework.config,最后根据宿主环境替换machine.config的指向。遍历最终的Config,找到请求的Section并缓存,文件下一次读取此Section时,会直接从SectionCache中取得,如果是其他Section,则重复以上过程。1.5 附录1.5.1 NamedConfigurationElement1.5.2 ServerInfoConfigurationElement1.5.3 TypeConfigurationElement1.5.4 IdentityConfigurationElement1.5.5 UriConfigurationElement2 Cache2.1 模块概述在公共组件库中的MCS.Library. Caching命名空间下包含了框架中实现以内存作为缓存介质的功能模块,该模块有如下特点:l 支持将缓存分类。l 缓存会随着依赖项的状态改变而失效l 缓存的统一管理,以及定期处理过期缓存。使用Cache模块需要4大类对象 自定义的缓存项,封装需要缓存的数据。 缓存依赖项,定义缓存项的过期依赖。 缓存容器,存放自定义的缓存项。 缓存管理器,注册缓存容器,并统一的管理所有缓存容器。2.2 使用Cache在一般应用程序中,我们总会使用Cache这种用空间换时间的方式来提高性能。Cache的实现方案多种多样,其中,通过在内存中的字典来实现Cache,是一种常见的方案。ObjectCacheQueue.Instance.Add(Name, Li An);看看下面的代码示例:在这段代码示例中,ObjectCacheQueue是公共组件库的一个Cache类,使用起来就象一个字典,和CLR的Dictionary十分相似。ObjectCacheQueue从CacheQueue派生,因此它的基本属性和方法都来自于泛型类CacheQueue。2.3 Cache的失效Cache中数据的更新和失效是所有解决方案中都会面临的难点。它意味着字典中的每一项的生存期,都会和一个特定的业务规则进行关联。当满足某些条件时,Cache字典中的值就会进行变更。在公共组件库中,这种机制是通过Cache依赖项来实现的。如果我们的Cache仅仅是一个字典,那么它就没有存在的必要。真正的核心特性将在下一节中进行介绍。2.4 Cache依赖项Cache中数据的更新和失效是所有解决方案中都会面临的难点。它意味着字典中的每一项的生存期,都会和一个特定的业务规则进行关联。当满足某些条件时,Cache字典中的值就会进行变更。在公共组件库中,这种机制是通过Cache依赖项来实现的。下面的代码,会指定某一个Cache项的过期时间。AbsoluteTimeDependency dependency = new AbsoluteTimeDependency(DateTime.Now.AddSeconds(10);ObjectCacheQueue.Instance.Add(Name, Li An, dependency);代码中的dependency变量,表示了一个依赖项,它和通过字典的Add方法和一个字典项关联在一起,决定了字典项有效期。在上面的代码中,AbsoluteTimeDependency是一个绝对时间过期的依赖项,例子中指定了10秒钟之后为过期时间。公共组件库提供了几种常见依赖项,如下表:类名说明AbsoluteTimeDependencyCache项到了某一时间点失效SlidingTimeDependencyCache项一段时间不使用后失效FileCacheDependency文件改变后,Cache项失效CookieCacheDependencyWeb应用特有的,某个cookie失效后,Cache项失效UdpCacheNotifierDependency当程序收到一个特定Udp包时,Cache项失效这些依赖项的具体介绍,会在附录中详细说明的。有一种特殊的依赖项叫做MixedDependency,它会将几种不同的依赖项组合在一起,其中有一个满足条件,就会使Cache项失效。如下所示:AbsoluteTimeDependency timeDependency =new AbsoluteTimeDependency(DateTime.Now.AddSeconds(600);FileCacheDependency fileDependency =new FileCacheDependency(C:Configapp.config, C:Configmachine.config);MixedDependency mixedDependency =new MixedDependency(timeDependency, fileDependency);ObjectCacheQueue.Instance.Add(Name, Li An, mixedDependency);代码中的MixedDependency可以将其它依赖项组合起来使用(包括MixedDependency自己),在上述例子中,文件变更或超时都会导致Cache项的失效。2.5 Cache数据的清理依赖项(Dependency)的使用提供了Cache数据失效的机制。在依赖项起作用的时候,用户访问Cache的时,会发现数据已经不见了。但是,这并不意味着数据从内存中完全消失。例如,如果使用AbsoluteTimeDependency和SlidingTimeDependency这种时间相关的依赖项,如果没有一个后台清理程序,是不可能自动地随着时间的推移清除内存中失效的Cache项的。至于FileCacheDependency和UdpCacheNotifierDependency,它们会在文件变更或接受到基于Udp协议的数据包后,从内存中删除数据。实际上,这两种依赖项也是有后台线程在进行监控状态变化。CookieCacheDependency比较特别,只有当前应用是Web应用,且有Http请求到达时,才能够知道当前请求中的Cookie是否还有效,因此,如果某个客户端一直没有发送Http请求,那么和CookieCacheDependency关联的Cache项就会一致保存在内存中。因此,建议使用CookieCacheDependency的Cache项最好还与AbsoluteTimeDependency、SlidingTimeDependency等其它依赖项进行关联。用户一般不用关心Cache项在内存中的生存期,但是内存的清理确实是需要考虑的一个问题。为了保证已经失效的Cache项不会长时间存在于内存中,公共组件库会启动一个后台线程,它会按照一个时间间隔来检查每一个Cache项是否失效。我们可以在配置文件中设置轮询的时间间隔。这段配置文件,定义了每隔180秒清理一次。这个属性可以不设置,缺省时间是10秒。这个清理线程是Background类型的,对于Windows Form应用,随着程序的退出自行结束。Web应用特殊一些,为了安全可靠起见,会在每次Http请求的时候,检查是否需要清理,如果超出了scanvageInternval所定义的轮询间隔就进行清理。Web应用需要在config(web.config)文件中配置一个HttpModule来实现此功能。2.5.1 自定义Cache依赖项所有的Cache依赖项都是从DependencyBase派生而来的,因此我们要使编写一个自己的Cache依赖项,也是从这个类派生下来即可。DependencyBase类的大致定义如下:public abstract class DependencyBase : IDisposablepublic CacheItemBase CacheItemget return this.cacheItem; / 属性,获取或设置Cache项最后修改时间的UTC时间值public virtual DateTime UtcLastModifiedget return this.utcLastModified; set this.utcLastModified = value; / 属性,获取或设置Cache项的最后访问时间的UTC时间值public virtual DateTime UtcLastAccessTimeget return this.utlLastAccessTime; set this.utlLastAccessTime = value; / 属性,获取此Dependency是否过期public virtual bool HasChangedget return false; / 当Dependency对象绑定到CacheItem时,会调用此方法。此方法被调用时,/ 保证Dependency的CacheItem属性已经有值internal protected virtual void CacheItemBinded()public virtual void Dispose()如果写一个自定义的Cache依赖项,最简单的实现就是从DependencyBase派生,并且重载HasChanged。public class MyFooDependency : DependencyBaseprivate bool expiredFlag = false;public MyFooDependency()public override bool HasChangedgetreturn expiredFlag;只要重载了HasChanged属性,访问Cache或清理Cache的时候,就可以根据这个属性来进行失效判断。象AbsoluteTimeDependency之类的依赖项,就是在HasChanged属性实现中对当前时间进行了判断。上面这个例子,关键是要看expiredFlag什么时候被置为true。由于后台清理程序或Cache字典会经常读取这个属性,因此设计时需要使读取这个属性的操作开销极小。有些Cache依赖项的实现就是非常简单,如AbsoluteTimeDependency,有些复杂一些了。例如,我们需要定期检查数据库中某个数据的时间戳来决定Cache的失效,这就是一个开销较大的操作。这种操作通常是启动一个后台线程来实现的,它定期检查数据库中的数据,然后更新expiredFlag。首先需要有一个合适的入口点来启动这个线程。由于DependencyBase的CacheItemBinded方法发生在Cache数据和依赖项被添加到Cache字典后,因此可以通过重载CacheItemBinded方法来实现此功能。请看下面的例子:public class MyFooDependency : DependencyBaseprivate bool expiredFlag = false;private static List regsiteredDependencies = new List();private static Thread monitorNotifyThread = null;protected override void CacheItemBinded()lock(regsiteredDependencies)regsiteredDependencies.Add(this);InitMonitorNotifyThread();private static void InitMonitorNotifyThread()lock (typeof(MyFooDependency)if (MyFooDependency.monitorNotifyThread = null)Thread thread = new Thread(new ThreadStart(MonitorThread);thread.IsBackground = true;MyFooDependency.monitorNotifyThread = thread;thread.Start();在这个例子中,通过一个静态变量monitorNotifyThread保证了只会启动一个监听线程。而另一个静态变量regsiteredDependencies,记录了所有使用中的MyFooDependency。接下来我们看看监听线程内部的实现逻辑:private static void MonitorThread()while (true)tryThread.Sleep(TimeSpan.FromSeconds(10);if (DBDataChanged()NotifyAllDependenciesChanged();catch (System.Exception)private static void NotifyAllDependenciesChanged()lock (regsiteredDependencies)foreach (MyFooDependency dependency in regsiteredDependencies)dependency.expiredFlag = true;监听线程MonitorThread每隔10秒钟检查数据库的中状态,如果发现数据变化,将所有相关依赖项的expiredFlagbia标记置为true。10秒钟的轮询间隔可以根据具体业务进行调整,不过需要注意的是,即使间隔再短(短到极致就是0),也需要执行Thread.Sleep来释放CPU的时间片。正如前文所述,真正Cache项的清除,是在后台清理程序执行后或访问该Cache项时。和真正的失效时间有一个延迟,要想在监听线程中同步清除,我们可以修改上面代码中的NotifyAllDependenciesChanged方法。private static void NotifyAllDependenciesChanged()lock (regsiteredDependencies)foreach (MyFooDependency dependency in regsiteredDependencies)dependency.CacheItem.RemoveCacheItem();每个依赖项都有一个对应的CacheItem属性,CacheItem也有一个Queue属性,来表示它所属于的Cache字典。CacheItem的RemoveCacheItem就是从字典中删除自己。2.6 缓存容器框架中包含三种类型的Cache存储容器l CacheQueue有长度限制的容器。这种容器的生命周期与应用程序相同,只有在应用程序被卸载的时候,才释放内存。l PortableCacheQueue没有长度限制的容器,与CacheQueue的生命周期一样。l ContextCacheQueueBase上下文缓存容器,该容器的生命期仅仅在当前线程(WinForm)或一次Http(Web)请求过程中有效,请求结束后,缓存容器随即释放。2.6.1 有长度限制的缓存容器(CacheQueue)有长度限制的容器,内部利用LruDictionary容器,来存储CacheItem。LruDictionary容器会把最近使用的Cacheitem放在最前面,而最少使用的CacheItem放在最后面,从而实现快速索引频繁使用的CacheItem。由CacheManager统一扫描,或者在超出容器大小时,删除依赖过期项。CacheQueue只能在配置文件中设置缓存项的个数,默认值是100。这种容器的生命周期与应用程序相同,只有在应用程序被卸载的时候,才释放内存。在这里,我们用两个不同类型的缓存容器来举例说明。 自定义缓存项/自定义的缓存项public class TestCacheItem1private int index;private string name;/自定义缓存项public class TestCacheItem2private int age;private string name; 自定义缓存容器定义两个装载项为上述两个类的缓存容器,在定义时需注意以下几点:l 必须继承自MCS.Library.Caching.CacheQueue.l 必须向CacheManager注册,否则无法实现定期清除过期缓存项。l 由于CacheManager 中使用类型作为Dictionary的Key,所以必须使用单件,即保证类实例的单一性。否则在添加缓存项时,会覆盖或无法添加。/定义一个缓存容器,此处必须为泛型封闭类,且必须继承CacheQueue类,装载项为TestCacheItem1public class TestCacheQueue1 : CacheQueue/向框架中的注册一个Cache容器,必须使用CacheManager.GetInstance来注册,否则无法实现定期清除过期缓存项/使用static readonly 关键字,将Instance定义为只读静态变量,初始化后不在改变。public static readonly TestCacheQueue1 Instance;/使用静态构造函数和私有构造,使客户端无法实例化此类,所有的初始化交给CLR来完成,保证其唯一性static TestCacheQueue1()/必须向CacheManage注册这个缓存容器,否则无法缓存也无法进行容器的定期扫描。Instance = CacheManager.GetInstance();private TestCacheQueue1() /自定义的缓存容器,装载项为TestCacheItem2public class TestCacheQueue2 : CacheQueue/向框架中的注册一个Cache容器,必须使用CacheManager.GetInstance来注册,否则无法实现定期清除过期缓存项/推荐写法。public static readonly TestCacheQueue2 Instance = CacheManager.GetInstance();private TestCacheQueue2() 配置文件写法配置文件需注意以下几点:l scanvageIntervalCacheManager的扫描间隔时间。l defaultQueueLength所有容器的默认长度,如果单个容器没有定义,则使用。l typeName缓存容器类型全名称。l queueLength单个容器的长度 Web页面调用代码给定长缓存容器添加CacheItem时,可以添加缓存依赖,那么在访问缓存项时,会判断依赖项的HasChanged属性,如果该属性为true,那么此缓存项过期,无法获取。如果缓存依赖项为空的话,那么此缓存项只有在添加缓存项时检测到缓存项的位置超出缓存容器设置长度时才会被清理,请注意。Web客户端1protected void btsubmit_Click(object sender, EventArgs e)string index=txtindex.Text;string name=txtname.Text;if(string.IsNullOrEmpty(index)return;string file1 = Server.MapPath(testfile1.txt);string file2 = Server.MapPath(testfile2.config);/创建一个缓存项,以TestCacheItem1的Index属性作为此缓存项的KeyTestCacheItem1 item=new TestCacheItem1(Convert.ToInt32(index),name);/创建一个缓存依赖项,此处为文件依赖类型,可以依赖多个文件/将文件物理路径作为初始化文件依赖的参数。FileCacheDependency files = new FileCacheDependency(file1,file2);/在TestCacheQueue1中加入一个缓存项,该缓存项,依赖于files/当文件发生更改时,缓存过期。如果依赖项为空,/那么此缓存项只有在添加缓存项时检测到此缓存项的位置超出缓存容器设置长度时才会被清理TestCacheQueue1.Instance.Add(item.Index, item, files);protected void btsearch_Click(object sender, EventArgs e)string index = txtindex.Text;if (string.IsNullOrEmpty(index)return;TestCacheItem1 item = null;/通过Key来获取缓存项。如果缓存中无此项,则返回false/如果依赖项为过期也返回falseif (TestCacheQueue1.Instance.TryGetValue(Convert.ToInt32(index),out item)this.txtname.Text = TestCacheQueue1:+item.Name;elsethis.txtname.Text = 缓存1依赖改变,缓存清空;Web客户端2protected void btsubmit2_Click(object sender, EventArgs e)string name = txtcachename.Text;string age = txtage.Text;if (string.IsNullOrEmpty(age)return;/创建一个缓存项,以TestCacheItem2的name属性作为此缓存项的KeyTestCacheItem2 item = new TestCacheItem2(name,Convert.ToInt32(age) );/创建一个绝对时间依赖项,在此时间之后,缓存过期AbsoluteTimeDependency time = new AbsoluteTimeDependency(Convert.ToDateTime(2011-1-10 15:25:00);/在TestCacheQueue2中加入一个缓存项,该缓存项,依赖于time/当时间到达时,缓存过期TestCacheQueue2.Instance.Add(item.Name, item,time);protected void btsearch2_Click(object sender, EventArgs e)string name = txtcachename.Text;TestCacheItem2 item = null;/通过Key来获取缓存项。如果缓存中无此项,则返回false/如果依赖项为过期也返回falseTestCacheQueue2.Instance.TryGetValue(name, out item);if (!Equals(item, null)this.txtname.Text = TestCacheQueue2:+item.Age.ToString();elsethis.txtage.Text = 缓存2依赖改变,缓存清空;2.6.2 无长度限制的缓存(PortableCacheQueue)无长度限制的缓存容器,内部使用Dictionary容器,来储存CacheItem,与CacheQueue类相比,此容器不可设置大小,但同样可以设置缓存过期依赖,并且由CacheManager统一扫描,删除缓存过期项。这种容器的生命周期与应用程序相同,只有在应用程序被卸载的时候,才释放内存。 自定义缓存项与CacheQueue一样,PortableCacheQueue可以存储任意自定义类型。/自定义缓存项public class TestCacheItem2private string name;private int age;public string Nameget return name; set name = value; public int Ageget return age; set age = value; public TestCacheItem2( string name,int age) = name;this.age = age; 自定义缓存容器定义一个装载项为上述类的缓存容器,在定义时需注意以下几点:l 必须继承自MCS.Library.Caching.ProtableCacheQueue.l 必须向CacheManager注册,否则无法实现定期清除过期缓存项。l 由于CacheManager 中使用类型作为Dictionary的Key,所以必须使用单件,即保证类实例的单一性。否则在添加缓存项时,会覆盖或无法添加。/定义一个缓存容器,此处必须为泛型封闭类,且必须继承PortableCacheQueue类,装载项为TestCacheItem2public class TestProtableCacheQueue:PortableCacheQueue/向框架中的注册一个Cache容器,必须使用CacheManager.GetInstance来注册,否则无法实现定期清除过期缓存项/使用static readonly 关键字,将Instance定义为只读静态变量,初始化后不在改变。初始化交给CLR来处理,保证类实例的单一性public static readonly TestProtableCacheQueue Instance = CacheManager.GetInstance();private TestProtableCacheQueue() Web页面调用代码给无长度限制缓存容器添加CacheItem时,可以添加缓存依赖,那么在访问缓存项时,会判断依赖项的HasChanged属性,如果该属性为true,那么此缓存项过期,无法获取。如果不添加缓存依赖项,那么此缓存项只有在应用程序卸载时才会被清理,请注意。protected void btsubmit3_Click(object sender, EventArgs e)string name = txtcachename.Text;string age = txtage.Text;if (string.IsNullOrEmpty(age)return;/创建一个缓存项,以
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 广东国际市场营销学自考试题及答案
- 乐器理论考试题及答案
- 老年康复考试题及答案
- 电声器件制造工抗压考核试卷及答案
- 有色金属配料工技能操作考核试卷及答案
- 课件无法预览的原因
- 咖啡制作考试题及答案
- 掘进支护考试题及答案
- 反射炉工协作考核试卷及答案
- 警示教育考试题及答案
- 中医脑病专科建设
- 《网络安全为人民、网络安全靠人民》网络安全主题班会课件
- T/CCT 004-2020煤用浮选起泡剂技术条件
- 2025CSCOCSCO宫颈癌的诊疗指南更新
- 《幽门螺杆菌检测》课件
- 职业技能等级认定考试保密协议书
- 教学评一体化:新课标下道德与法治教学的必然选择
- 免还协议合同样本
- 脑出血病人的护理
- 纪念抗日战争胜利80周年心得体会
- T-ZSA 288-2024 餐饮设备智能烹饪机器人系统通.用技术要求
评论
0/150
提交评论