当前位置: 首页 > 编程日记 > 正文

多样化实现Windows phone 7本地数据访问5——深入Rapid Repository

上一篇多样化实现Windows Phone 7本地数据访问<4>——Rapid Repository  中初步的介绍Repid Repository作为Windows phone 7数据库存储原理Repid具有特点以及数据CRUD基本操作.Rapid Repository 是一个基于WP7开源的数据库. 上周联系Rapid 数据库的作者Sean McAlinden.有人也针对下一个版本的做出相关询问.Sean McAlinden明确提出下个版本中将会加入对视图/索引模型的支持,提高查询的性能,以及对事务的支持.也就是在保持数据性能和完整性上两个方向上继续完善Rapid.

本篇将对于Rapid数据存储后台处理机制一些具体细节进行逐步说明. 这样做目的是把Rapid数据处理细节流程完整的暴露出来. 以及Rapid采用Linq查询,数据的延迟加载,数据的缓存处理.等.进一步说明.

<1>Rapid Repository数据处理机制

Rapid Repositroy是开源的,你可以在http://rapidrepository.codeplex.com/ 上下载到Rapid的源码以及作者写的相关数据处理相关Demo.

2010-11-25_095954

对于移动平台的数据库都能找到相关开源的替代解决方案.对于目前WP7因基于Silverlight平台构建特点..对于数据本地存储选择一直都没有得到官方的支持,但开源社区的力量总是能为我们开辟一些基于WP7目前架构特点独特数据存储视角. Rapid也就在本月份刚发布1.0正式版.相对于Sqlite和Effect DB4O Rapid算是后来者. WP7上本地DB选择会随着WP7开发需求增加会逐渐变多, 而对于想深入理解WP7数据存储处理机制,Rapid开源的特点无不给了我们"窥视”WP7不被暴露的数据处理机制的极好机会.

下载源码成后进行解压 打开Rapid解决方案 预览源码结构:

2010-11-25_102212

我们大概就能从作者对源码归类初步能了解Rapid具有功能.对于类之间关联关系做了简单归纳, VS2010中的Class DiaGram看的不太清楚.做了一幅图:

2010-11-25_111015

如上看到是存在类与类存在关联关系的一部分.[其他类相对比较独立]

IRapidEntity则是对所有存储实体类Entity定义一个唯一标识的属性Guid.

ISerialiser则是对应对实体的序列化和反序列操作[和Serialiser类相对应]

IFileManager实体序列化完成后产生一些序列化后文件存在在WP7独立存储空间 FileManager则是对这些序列化后实体文件管理类.

现在从添加一条数据记录方式来跟踪一下Rapid如何把数据存储DB中整个过程,我们先定义一个Customer类:

   1:  public class Customer : IRapidEntity
   2:      {
   3:          public Guid Id { get; set; }
   4:          public string FirstName { get; set; }
   5:          public string LastName { get; set; }
   6:          public Address Address { get; set; }
   7:          public List<ContactNumber> ContactNumbers { get; set; }
   9:          public Customer()
  10:          {
  11:              this.ContactNumbers = new List<ContactNumber>();
  12:          }
  13:      }

Customer类继承IRapidEntity接口目的实现对实体类的唯一标识属性Guid. 定义CustomerRepossitory数据库操作类:

   1:   public class CustomerRepository : RapidRepository<Customer>
   2:      {
   3:          /// <summary>
   4:          /// Add new Customer Entity to Repid DB
   5:          /// Sign by chenkai Data:2010年11月14日11:12:35
   6:          /// </summary>
   7:          /// <param name="entity">Customer Entity</param>
   8:          /// <returns>Add Customer</returns>
   9:          public override Customer Add(Customer entity)
  10:          {
  11:              var customer = base.Add(entity);
  12:              RapidContext.CurrentContext.SaveChanges();
  13:              return customer;
  14:          }
  15:      }

CustomerRepository类继承RapidRepository所有数据库相关操作,当我们添加WP7一个Customer实体时:

2010-11-25_112155

2010-11-25_113401

当我们点击Save按钮时Repid按钮将把Customer实体保存Rapid本地数据库中, 跟踪源码发现Save方法下操作调用CustomerRepository 的父类RapidRepository的Add()方法, 具体实现:

   1:    public virtual TEntity Add(TEntity entity)
   2:          {
   3:              Contract.Requires("entity", entity != null);
   5:              RapidContext.CurrentContext
   6:              .AddOperationRequest(new OperationRequest(entity, typeof(TEntity), OperationRequestType.Add));
   7:              return entity;
   8:          }

其中Contract.Requires()方法调用初始实体对象对非空异常的进行独立封装. RapidContext则是获取Repid数据库操作类的上下文信息. CurrentContext获取当前保存操作上下文对象.看一下定义:

   1:  public static RapidContext CurrentContext
   2:          {
   3:              get
   4:              {
   5:                  if (context == null)
   6:                  {
   7:                      lock (syncRoot)
   8:                      {
   9:                          if (context == null)
  10:                            context = new RapidContext();   
  11:                      }
  12:                  }
  13:                  return context;
  14:              }
  15:          }

显然加入一个Lock锁机制保证每次保存Entity操作对象在整个上下文中唯一.

AddoperationRequest()则把要保存的实体类封装成对后台Rapid数据库一次操作请求OperationRequest.添加到RepidContex属性List<OperationRequest> operationRequests集合中:

   1:          /// <summary>
   2:          /// Adds the operation request.
   3:          /// </summary>
   4:          /// <param name="operationRequest">The operation request.</param>
   5:          public void AddOperationRequest(OperationRequest operationRequest)
   6:          {
   7:              Contract.Requires("operationRequest", operationRequest != null);
   8:              this.operationRequests.Add(operationRequest);
   9:          }

我们看一下一个操作请求OperationRequest封装:
   1:          /// <summary>
   2:          /// OperatorRequest Static Struct
   3:          /// Sign by chenkai Data:2010年11月15日11:59:07
   4:          /// </summary>
   5:          public OperationRequest(object entity, Type entityType, OperationRequestType operationRequestType)
   6:          {
   7:              Contract.Requires("entity", entity != null);
   8:              Contract.Requires("entityType", entityType != null);
  10:              if (operationRequestType != Context.OperationRequestType.Add)
  11:              {
  12:                  this.EntityId = ((IRapidEntity)entity).Id;
  13:              }
  15:              this.Entity = entity;
  16:              this.EntityType = entityType;            
  17:              this.OperationRequestType = operationRequestType;
  18:          }

当我们保存一个实体类时,OperatorRequest类作为对后台数据库一次操作请求,则进一步对添加Entity进行封装.保存实体信息同时指定实体类型.EntityID则是实体的唯一标识GuID.这时返回封装操作请求全部添加在List<OperationRequest> operationRequests集合中, 这个请求会一直呆在OperatroContext上下文直到调用了SaveChanges()才回给实体创建一个Guid唯一标识并保村Rapid数据库,如何来保存?SaveChange()方法定义:

   1:      /// <summary>
   2:          /// Saves the changes.
   3:          /// </summary>
   4:          public void SaveChanges()
   5:          {
   6:              foreach (OperationRequest request in this.operationRequests)
   7:              {
   8:                  switch (request.OperationRequestType)
   9:                  {
  10:                      case OperationRequestType.Add:
  11:                          ((IRapidEntity)request.Entity).Id = Guid.NewGuid();
  12:                          var addString = this.serialiser.Serialise(request.EntityType, request.Entity);
  13:                          this.fileManager.Save(request.EntityType, ((IRapidEntity)request.Entity).Id, addString);
  14:                          AddToCache(request.EntityType, (IRapidEntity)request.Entity);
  15:                          break;
  16:                  }
  17:              }
  18:           }

当添加完实体只有在调用SaveChange()方法数据的更改才回体现到Rapid数据库中. 同样如果要是所有对实体Entity更改得到持久化文件方式保存,SaveChange()则是必须要核心调用的.

Savechange()中当一个实体进入经过OperatorRequest操作请求的封装.则可以通过OperationRequestType枚举在判断当前对实体操作类型.[添加]. 判断成功后这时才回对实体生成一个唯一标识的GUID. 然后通过Serialise()方法对实体Customer进行序列化成JSon格式字符串返回.接着FileManager通过Save()方法把实体序列化后JSon字符串以文件方式存储在WP7的独立存储空间上. 进阶着把操作的实体类信息在缓存中进行更新.保证每次数据库操作能在第一时间和缓存中数据进行同步.

自此Rapid保存数据方式具体流程 就全盘而出. 制图如下:

2010-11-25_124706

如上就是Rapid数据处理数据整个流程的细节.整体来看并不复杂, Rapid 定位是WP7的文档数据即NoSQl.数据持久化存储方式同样还是用独立存储中建立Json格式的文件. 但是Rapid的处理数据方式上相对于WindowsPhone DB和Effiect有些不同.这个稍后我会说到.

<2>Rapid Repository缓存处理

在上一篇中多样化实现Windows Phone 7本地数据访问<4>——Rapid Repository曾提出关于Rapid数据性能上质疑,当时我并没有看到Rapid的源码.后来在Rapid的作者关于数据性能表现留言中,提到Rapid采用缓存机制来处理这方面需求. 比较感兴趣.作者Sean McAlinden也针对这个问题做了一个相关说明. 这里暂且不提 先回到源码中来 saveChange()方法中缓存操作来.

   1:     case OperationRequestType.Add:
   2:          AddToCache(request.EntityType, (IRapidEntity)request.Entity);
   3:          break;
   4:     case OperationRequestType.Update:
   5:          EntityCache.Update(request.EntityType, request.EntityId, request.Entity);
   6:          break;
   7:     case OperationRequestType.Delete:
   8:          EntityCache.Remove(request.EntityType, request.EntityId);
   9:          break;

如上可以看出,Rapid中默认情况下在数据增加/修改/删除中添加或更新缓存保持数据同步,类似做相关数据查询所有数据全部缓存中已经同步,查询数据源也就从序列化后Json文件转到内存中来,这样一来性能会大大提升.

类似我们现在又一个场景,在WP7中去搜索一个Customer实体:

2010-11-25_135048

2010-11-25_135104

当点击Search按钮搜索.Rapid支持对象之间Linq操作.类似在搜索中可以指定搜索的选项指定过滤Customer多个字段:

   1:          /// <summary>
   2:          /// Search Customer by Key Word
   4:          /// </summary>
   5:          private static List<Customer> GetCustomers(string searchText)
   6:          {
   7:              CustomerRepository repository = new CustomerRepository();
   8:   
   9:              //get all()方式 没有用到Cache.
  10:              return repository.GetAll().Where(x =>
  11:                  x.FirstName.ToLower().Contains(searchText.ToLower()) ||
  12:                  x.LastName.ToLower().Contains(searchText.ToLower()) ||
  13:                  x.Address.PostCode.ToLower().Contains(searchText.ToLower())).ToList();
  14:          }

如上把搜索框中内容搜索覆盖到Customer实体的FirstName/LastName/Address三个字段属性中查找.注意这里采用数据是GetAll()方式通过Where关键条件进行过滤,并没有采用缓存方式.

类似我们现在要提高用户体验要求.在加载搜索页时自动加载数据到内存中, 无需通过GetALL()去查找文件 进行IO的读写.而Rapid正好支持这种数据预加载方式放到缓存中存储. 加载过程作为后台线程单独处理,无须主线程等待缓存加载完成才做出响应. 而针对不同实体这种Rapid预加载方式只能运行一次.当时我很不理解,后来作者回复中, 类似在SearchCustomer.Xaml的initialise 方法中 当页面第一次加载时才被调用,作者的解释这样处理对于实体预加载只做一次的处理 是为了日常数据中合理管理操作缓存. 预加载方式:

   1:  //预加载
   2:  1.EntityCache.EagerLoad<Customer>();

针对这个问题,作者也提到已经打算在下一个版本加以完善. 另外有人也提出一个问题:

“当我们编写Windows Phone 7 Appliction时 自己机器配置内存有限 Rapid缓存方式必然会占用一部分内存, 如果关闭Rapid缓存释放更多内存资源”.

作者也针对这个问题做了很好处理: 可以在我们Wp7 appliction 初始化方法及App.xaml中InitializePhoneApplication()方法中.直接过滤掉指定实体的缓存设置. 其实这里你应该能猜到后台处理缓存方式利用实体的类型即Entity.Type方式进行标识的:

   1:  public MyApplicationStartup()
   2:  {
   3:      //取消缓存设置
   4:      EntityCache.NoCache<Customer>();
   5:  }

每次加载类型是在从缓存中排除的实体类型.

篇幅有限 这里不在赘述Rapid对缓存处理原理介绍.从上面介绍预加载方式应该能看出端倪.如果想连接更多请查看Rapid源码.

<3>Rapid Repository小结

如上基本介绍Rapid数据库在Windows  Phone 7上数据处理原理和缓存机制. 说道Rapid采用缓存与Json格式文件存储进行同步.这种方式相对Sqlite,Efficet WindowsPhone DB都有些不同.  总结关系图如下:

2010-11-25_145407

EffiectDB方式内存和文件模式在数据进行访问只能选择一种方式进行.

SQlite方式则更加单一只能以一个硬盘上文件持久化数据.每次文件读写都需要固定的IO读取操作.

DB4O和Windows Phone DB 则采用WP7的独立存储机制写入文件流方式存储.

Rapid方式内存和文件模式同时可以共存,数据来源可以有用户决定. 内存模式可以禁用.这样一来相对于Efficet DB就显得非常灵活.把真正使用权交给用户来决定.

如果说Windows Phone DB在Wp7上采用独立存储方式模拟一个shcme,表,字段属性关系型数据库.是具有开创性的那么Rapid则灵活使用数据存储并对以用户对数据要求为核心积极数据反馈方式则是非常实用的. SQlite则一直传统的File模式,DB4O则没有成熟尚需时日.

针对Windows Phone 7本地数据库特点 有时间会整理出一个总结篇,详细比较Sqlite,DB4O,Windows PhoneDB Rapid,Efficet等数据处理的优势.如上本篇关于Rapid在数据处理原理上一次详细解析.如有疑问请在留言中提出.

相关文章:

调试Tomcat源码

需要调试Tomcat源码其实很简单&#xff0c; 1.保持你的Tomcat安装文件和源码是版本一致 http://tomcat.apache.org/download-80.cgi 下载安装版和源码2个版本 2.建立Java自由格式项目 先在IDE里配置好Tomcat&#xff0c;这个不复杂。 然后新建一个项目&#xff0c;这个需要…

开源 免费 java CMS - FreeCMS1.9 全文检索

项目地址&#xff1a;http://code.google.com/p/freecms/ 全文检索 从FreeCMS 1.7開始支持 仅仅有创建过索引的对象才干被lucene类标签查询到。 信息类数据会在信息更新、审核、删除、还原操作时自己主动进行全文检索处理。1. 创建索引 从左側管理菜单点击创建索引进入。 您能够…

Spring Mock单元测试

针对post和get import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.…

tar、gzip、gunzip、bzip2、zip、unzip

tar [参数] 文件或目录名 •参数&#xff1a; -c&#xff1a; 建立新的备份档文件。 -f&#xff1a; 对普通文件进行操作。这个参数通常是必选的。 -r&#xff1a; 向备份档文件追加文件。 -x&#xff1a; 从备份档文件中解出文件。 -t&#xff1a; …

港科大谢丹阳教授问诊未来,预测长远趋势与转折点

阳春三月&#xff0c;万象更新&#xff0c;2020年注定是不平凡的一年&#xff01;有激荡就会遇见变革&#xff0c;有挑战就会迎来机遇。今天总会过去&#xff0c;未来将会怎样&#xff1f;香港科大商学院内地办事处重磅推出全新升级的《袁老师访谈录》全新系列【问诊未来院长系…

数字签名与数字证书

先看一下百度百科对数字签名和数字证书的解释&#xff1a; 数字签名&#xff1a; 将报文按双方约定的HASH算法计算得到一个固定位数的报文摘要。在数学上保证&#xff1a;只要改动报文中任何一位&#xff0c;重新计算出的报文摘要值就会与原先的值不相符。这样就保证了报文的不…

Spring源码分析【3】-SpingWebInitializer的加载

SpingWebInitializer的加载 Spring基于注解的配置代码&#xff1a; public class SpingWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {Overrideprotected Class<?>[] getRootConfigClasses() {return new Class<?>[]{RootCon…

PyTorch 1.5发布,与AWS联手推出TorchServe

导读&#xff1a;近日 PyTorch 发布了 1.5 版本的更新&#xff0c;作为越来越受欢迎的机器学习框架&#xff0c;PyTorch 本次也带来了大的功能升级。此外&#xff0c; Facebook 和 AWS 还合作推出了两个重要的 PyTorch 库。作者 | 神经星星来源 | HyperAI超神经&#xff08;ID:…

更改时区,时间

整更改ubuntu时区,时间,localePublished by 笨二十一 at 11:56 上午 under Linux/Unix,服务器更改时区&#xff0c;时间执行tzselect按照提示进行选择时区sudo cp /usr/share/zoneinfo/Asia/ShangHai /etc/localtime执行sudo ntpdate cn.pool.ntp.orgcn.pool.ntp.org是位于中国…

throttle与debounce的区别

前几天看到一篇文章&#xff0c;我的公众号里也分享了《一次发现underscore源码bug的经历以及对学术界拿来主义的思考》具体文章详见&#xff0c;微信公众号&#xff1a;文中讲了大家对throttle和debounce存在误解&#xff0c;同时提到了《高程3》中实现节流方法存在一些问题&a…

Spring源码分析【0】-框架的基础:继承和接口调用链

Spring源码大量的使用继承和接口调用&#xff0c;现举个例子&#xff0c;不搞清楚这个无法看代码。 public class A extends B{public void f1() {System.out.println("f1 in a");} }public abstract class B extends C {protected abstract void f1();protected Str…

华人计算机视觉科学家黄煦涛逝世,众多AI大牛发文缅怀

当地时间2020年4月25日&#xff0c;华人计算机科学家黄煦涛教授在美国印第安纳州逝世&#xff0c;享年84岁。黄煦涛教授主要从事教学与图像处理、模式识别、计算机视觉和人机交互等方面的研究工作&#xff0c;一生出版了 14 本书&#xff0c;发表了 400 多篇学术论文。在学术研…

七年之痒,再见理想

不确定“再见理想”是“再见了&#xff0c;理想”还是“再次燃起理想”&#xff0c;稀里糊涂地对这句话有感觉。作为程序员&#xff0c;总会有自己的技术价值观和技术理想。工作七年多&#xff0c;开始痒了。 程序员的生活总是喜忧参半&#xff0c;出入体面的写字楼&#xff0c…

HTML5学习笔记二 HTML基础

一、HTML 标题 HTML 标题&#xff08;Heading&#xff09;是通过<h1> - <h6> 标签来定义的. <h1>标题一</h1> <h2>标题二</h2> <h3>标题三</h3> 二、HTML 段落 HTML 段落是通过标签 <p> 来定义的. <p>亲吻你的手还…

程序员感叹一年只能存下15万太少了……网友:潸然泪下

最近有程序员网友晒出自己的年终奖&#xff0c;税后高达15.7万&#xff01;看到这个情形&#xff0c;很多网友表示自己“被打鸡血了”。他强调学习的重要性&#xff0c;学习仍然是在这个时代下&#xff0c;普通人能够逆袭&#xff0c;给家人更好生活的一把利器&#xff01;今天…

Spring源码分析【2】-Tomcat和Sping的连接点

Tomcat是怎么调用上Spring的呢&#xff1f;需要找到这个连接点。 答案就在org.apache.catalina.startup.ContextConfig的processServletContainerInitializers方法 new WebappServiceLoader() 回到processServletContainerInitializers 进入org.apache.catalina.startup.Weba…

优化数据库的方法及SQL语句优化的原则

优化数据库的方法&#xff1a; 1、关键字段建立索引。 2、使用存储过程&#xff0c;它使SQL变得更加灵活和高效。 3、备份数据库和清除垃圾数据。 4、SQL语句语法的优化。&#xff08;可以用Sybase的SQL Expert&#xff0c;可惜我没找到unexpired的序列号&#xff09; 5、清理删…

各大浏览器 CSS3 和 HTML5 兼容速查表

2019独角兽企业重金招聘Python工程师标准>>> 不知不觉中&#xff0c;支持 CSS3 和 HTML5 的浏览器变得越来越多&#xff0c;甚至包括最新版的 IE&#xff0c;当然&#xff0c;所谓支持仅仅是部分支持&#xff0c;因为 CSS3 和 HTML5 的W3C 规范都尚未形成。如果你现…

Spring源码分析【1】-Tomcat的初始化

org.apache.catalina.startup.ContextConfig.configureStart() org.apache.catalina.startup.ContextConfig.webConfig() 进入org.apache.catalina.startup.ContextConfig.processServletContainerInitializers processServletContainerInitializers参考&#xff1a;Spring源…

360金融首席科学家张家兴:只靠AI Lab做不好AI中台 | 独家专访

「AI 技术生态论」 人物访谈栏目是 CSDN 发起的百万人学 AI 倡议下的重要组成部分。通过对 AI 生态顶级大咖、创业者、行业 KOL 的访谈&#xff0c;反映其对于行业的思考、未来趋势判断、技术实践&#xff0c;以及成长经历。 本文为 「AI 技术生态论」系列访谈第21期&#xff0…

Delphi 正则表达式语法(3): 匹配范围

// [A-Z]: 匹配所有大写字母var reg: TPerlRegEx; begin reg : TPerlRegEx.Create(nil); reg.Subject : CodeGear Delphi 2007 for Win32; reg.RegEx : [A-Z]; reg.Replacement : ◆; reg.ReplaceAll; ShowMessage(reg.Subject); //返回: ◆ode◆ear ◆elphi 200…

基础算法整理(1)——递归与递推

程序调用自身的编程技巧称为递归&#xff08; recursion&#xff09;。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法&#xff0c;它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解&…

php正则表达式函数 preg_replace用法

preg_replace (PHP 3> 3.0.9, PHP 4 ) preg_replace -- 执行正则表达式的搜索和替换说明 mixedpreg_replace( mixed pattern, mixed replacement, mixed subject [, int limit])在 subject 中搜索 pattern 模式的匹配项并替换为 replacement。如果指定了 limit&#xff0c;则…

面试官吐槽:“Python程序员就是不行!”网友:我能把你面哭!

最近几年&#xff0c;Python莫名火了起来&#xff0c;很多公司都想赶上这“莫名”的热潮&#xff0c;招聘到大牛人才。但是&#xff0c;最近一个HR在社交网站的吐槽又火了&#xff1a;那么问题来了&#xff0c;市面上为什么鲜有企业满意的优秀的Python程序员&#xff1f;企业到…

Spring源码分析【5】-Spring MVC处理流程

org.apache.catalina.core.ApplicationFilterChain.doFilter 获取Filter org.apache.catalina.core.ApplicationFilterChain.internalDoFilter org.springframework.web.filter.DelegatingFilterProxy.doFilter invokeDelegate org.springframework.security.web.FilterCha…

Mysql——外键

2019独角兽企业重金招聘Python工程师标准>>> 一&#xff0c;外键 外键&#xff1a;foreign key&#xff0c;&#xff08;外边的键&#xff0c;键不在本表中&#xff09;&#xff1a;如果一张表中有一个字段&#xff08;非主键&#xff09;指向另一张表的主键&#x…

揭开「拓扑排序」的神秘面纱

作者 | 小齐本齐责编 | Carol来源 | 码农田小齐Topological sort 又称 Topological order&#xff0c;这个名字有点迷惑性&#xff0c;因为拓扑排序并不是一个纯粹的排序算法&#xff0c;它只是针对某一类图&#xff0c;找到一个可以执行的线性顺序。这个算法听起来高大上&…

Spring源码分析【6】-ThreadLocal的使用和源码分析

Spring代码使用到了ThreadLocal java.lang.ThreadLocal.set getMap java.lang.Thread.threadLocals定义 回到set 如果map为null 则createMap

《软件的破解》

本人根据自己的经验简单给大家谈一谈。这些问题对于初学者来说常常是很需要搞明白的&#xff0c;根据我自己的学习经历&#xff0c;如果你直接照着很多破解教程去学习的话&#xff0c;多半都会把自己搞得满头的雾水&#xff0c;因为有很多的概念要么自己不是很清楚&#xff0c;…

php级别鉴定

一、PHP初级程序员薪资水平&#xff1a;4000.00--8000.00&#xff08;RMB/月&#xff09;~HTML设计与应用~DIVCSS~PHP基础~MySQL基础~PHP高级~CMS系统二、PHP中级程序员 薪资水平&#xff1a;8000.00--12000.00&#xff08;RMB/月&#xff09;~PHP面向对象~MySQL高级~Smarty模板…