zoukankan      html  css  js  c++  java
  • xEasyApp之后端的介绍

    前文我已经说了,为了能够让大家更好的理解xjplugin如何在asp.net mvc中应用,我编写了这样一个demo,本篇博文简要的说明下xEasy的结构,讲解一下ASP.NET MVC 和xjplugin 之外的东西。从这里下载到代码。要求安装了VS2010 和ASP.NETMVC3.0.打开解决方案,我们可以看到有两个主要的项目和一个解决方案文件夹,如下图所示:

    image

    其中xEasyApp.Web为网站, 包括视图,控制器,视图所需的特殊的Model 和js,css,image等文件。

    xEasyApp.Core则包含项目的业务逻辑层,数据访问层,和一起一些公用的类,如异常,配置读取类等。MVC中的Model在这里。。

    refdll中是项目中所引用的外部DLL,这里只有一个StructureMap (这时候一个IOC的类库,非常轻巧,也方便使用)。

    1: 利用T4模板生成数据访问

    也许还有人问T4是什么,关于T4的一些细节大家可以参考这里的几篇文章:

    http://msdn.microsoft.com/zh-cn/library/dd820620.aspx

    http://t4-editor.tangible-engineering.com/How-Do-I-With-T4-Editor-Text-Templates.htm

    http://www.cnblogs.com/artech/archive/2010/10/23/codegeneration_t4.html

    说到底它是一个基于模板的代码生成工具,其中模板又可以用C#编写,所以很方便哦

    在xEasyApp中利用T4模板根据数据库的结构生成基本的访问代码(它生成的不是一个完整的ORM框架,事实上如果要做的话是可以的。要知道Entity Framework就是基于T4的代码生成),在生成代码之前,我编写了两个基类用来包含一些功能的方法和属性,以便在编码的更好的调用

    一个是:BaseRepository :定义了数据访问的基本方法如ExcuteDataReader,ExcuteDataTable,ExecuteScalar,ExcuteNoQuery,还有执行相关存储过程的方法。 如果你在实际的项目中需要更好的控制连接池,或者数据访问层,可以对这个基类进行调整。xEasyApp现在是使用的SqlHelper我相信已经可以满足大部分的企业内部应用需求。

    另外一个是:BaseEntity 定义数据实体的基类,定义了实体状态的一些基本方法,如变更的字段,是否为新增记录等等。

    另外还定义了存储过程的封装。

    image

    定义了五个模板分别生成数据访问,实体和存储过程访问封装,其中Settings.ttinclude 定义的是配置和配置读取类,还有一些最基本的,如表信息,列信息的描述等,而SQLServer.ttinclude则包含读取数据库中

    Repositories.tt 是生成数据访问类,每个表一个类,会生成新增更新和删除,获取方法。

      1:  <#@ template language="C#v3.5" debug="False" hostspecific="True"  #>
      2:  <#@ output extension=".cs" #>
      3:  <#@ include file="SQLServer.ttinclude" #>
      4:  <# var tables = LoadTables();#>
      5:  //=============================================
      6:  // 该
      7:  // 生 <#= DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") #>
      8:  // =============================================
      9:  using System;
     10:  using System.Data;
     11:  using System.Text;
     12:  using System.Data.SqlClient;
     13:  using System.Collections.Generic;
     14:  
     15:  
     16:  namespace <#=Namespace#> {
     17:      
     18:  <#  foreach(var tbl in tables){
     19:          if(!ExcludeTables.Contains(tbl.Name))
     20:          {
     21:            var pkColumn = tbl.Columns.SingleOrDefault(x => x.Name.ToLower().Trim() == tbl.PrimaryKey.ToLower().Trim());
     22:           
     23:  #>
     24:          /// <summary>
     25:          /// Table: <#=tbl.Name#>
     26:          /// Primary Key: <#=tbl.PrimaryKey#>
     27:          /// </summary>
     28:          public partial class <#=Cleans(tbl.CleanName)#>Repository:BaseRepository 
     29:          {            
     30:              public void Save(<#=Cleans(tbl.CleanName)#> item)
     31:              {
     32:                  if(item.IsNew)
     33:                  {
     34:                      Insert(item);
     35:                  }
     36:                  else
     37:                  {
     38:                      Update(item);
     39:                  }
     40:              }    
     41:              public <#=Cleans(tbl.CleanName)#> Get(<#=pkColumn.SysType#> key)
     42:              {
     43:                  string sql = "SELECT <#
     44:  int i=0;
     45:  foreach(var col in tbl.Columns){
     46:      if(i>0)
     47:      {
     48:          #>,<#
     49:      }
     50:      #>[<#= col.Name #>]<#
     51:      i++;
     52:  }
     53:  #> FROM [<#=tbl.Name#>] WHERE [<#=tbl.PrimaryKey#>]=@<#=tbl.PrimaryKey#>";
     54:                  SqlParameter p =new SqlParameter("@<#=tbl.PrimaryKey#>",key);
     55:                  <#=Cleans(tbl.CleanName)#> item =null;
     56:                  using(IDataReader reader = base.ExcuteDataReader(sql,p))
     57:                  {
     58:                      if(reader.Read())
     59:                      {
     60:                          item =new <#=Cleans(tbl.CleanName)#>();
     61:                          <#
     62:                      i=0;
     63:                      foreach(var col in tbl.Columns){
     64:                              if(col.IsNullable)
     65:                              {
     66:                         #>if(!reader.IsDBNull(<#=i#>))
     67:                           {
     68:                              item.<#= col.CleanName #> = <#=GetReadFormatString("reader.",col.DataType,i)  #>;
     69:                           }
     70:                           <# }
     71:                              else
     72:                              {
     73:                                  #>
     74:  item.<#= col.CleanName #> = <#=GetReadFormatString("reader.",col.DataType,i)  #>;
     75:                              <#
     76:                              }                        
     77:                          i++;
     78:                      }#>
     79:  
     80:                      }
     81:                  }
     82:                  return item;
     83:              }
     84:              public int Delete(<#=pkColumn.SysType#> key)
     85:              {
     86:                  string sql ="DELETE FROM [<#=tbl.Name#>] WHERE [<#=tbl.PrimaryKey#>]=@<#=tbl.PrimaryKey#>";
     87:                  SqlParameter p =new SqlParameter("@<#=tbl.PrimaryKey#>",key);
     88:                  return base.ExecuteNonQuery(sql,p);    
     89:              }
     90:              public void Insert(<#=Cleans(tbl.CleanName)#> item)
     91:              {
     92:                  <#
     93:              bool pkAutoIncrement = false;
     94:  #>
     95:  string sql="INSERT INTO [<#=tbl.Name#>] (<# i = 0; foreach (var col in tbl.Columns)
     96:   {
     97:      if (col.AutoIncrement) { if (!pkAutoIncrement) { pkAutoIncrement = col.IsPK; }; continue; } if (i > 0)
     98:      {#>,<#}#>[<#= col.Name #>]<# i++;}#>) VALUES (<# i=0;foreach(var col in tbl.Columns){ if(col.AutoIncrement){continue;} if(i>0){#>,<#}#>@<#= col.CleanName #><# i++;}#>)";
     99:                  List<SqlParameter> SPParams = new List<SqlParameter>();
    100:                  <#foreach(var col in tbl.Columns){ if(col.AutoIncrement){continue;}#>
    101:  SPParams.Add(new SqlParameter("@<#=col.CleanName#>",item.<#=col.CleanName#>));    
    102:                  <#}#>
    103:  <#
    104:   if(pkAutoIncrement)
    105:   {
    106:  #>
    107:  sql +=";SELECT Scope_Identity()";
    108:                  object o = base.ExecuteScalar(sql, SPParams.ToArray());
    109:                  if(o!=null){
    110:                      item.<#=tbl.PrimaryKey#> =Convert.ToInt32(o);
    111:                  }
    112:  <#
    113:   }
    114:  else
    115:  {
    116:  #>
    117:  base.ExecuteNonQuery(sql, SPParams.ToArray());
    118:  <#
    119:      }
    120:  #>
    121:              }
    122:              public void Update(<#=Cleans(tbl.CleanName)#> item)
    123:              {
    124:                  if(item.ChangedPropertyCount>0)
    125:                  {
    126:                      StringBuilder sqlbuilder = new StringBuilder();
    127:                      sqlbuilder.Append("UPDATE [<#=tbl.Name#>] SET ");
    128:                      Dictionary<string,string> cols =new Dictionary<string,string>();
    129:                      <#foreach(var col in tbl.Columns){ if(col.IsPK){continue;}#>
    130:  cols.Add("<#= col.CleanName #>","[<#= col.Name #>]");
    131:                      <#}#>
    132:  int i = 0;
    133:                      //UPDATE COLUMNS
    134:                      foreach (string p in item.ChangedPropertyList)
    135:                      { 
    136:                          if(!cols.ContainsKey(p))
    137:                          {
    138:                              continue;
    139:                          }
    140:                          if (i > 0)
    141:                          {
    142:                              sqlbuilder.Append(",");
    143:                          }
    144:                          sqlbuilder.AppendFormat("{0}=@{1}", cols[p], p);
    145:                          i++;
    146:                      }
    147:                      //WHERE;
    148:                      sqlbuilder.Append(" WHERE [<#=tbl.PrimaryKey#>]=@<#=tbl.PrimaryKey#>");
    149:  
    150:                      List<SqlParameter> SPParams = new List<SqlParameter>();
    151:                      <#foreach(var col in tbl.Columns){#> <#if(!col.IsPK){#>    
    152:                      if(item.IsChanged("<#=col.CleanName  #>"))
    153:                      {
    154:                          SPParams.Add(new SqlParameter("@<#=col.CleanName#>",item.<#=col.CleanName#>));    
    155:                      }<#}else{#>
    156:  SPParams.Add(new SqlParameter("@<#=col.CleanName#>",item.<#=col.CleanName#>));    
    157:  <#}}#>
    158:  
    159:                      base.ExecuteNonQuery(sqlbuilder.ToString(), SPParams.ToArray());
    160:                  }
    161:              }
    162:              public List<<#=Cleans(tbl.CleanName)#>> QueryAll()
    163:              {
    164:                  string sql ="SELECT <# i=0;foreach(var col in tbl.Columns){ if(i>0){#>,<#}#>[<#= col.Name #>]<# i++;}#> FROM [<#=tbl.Name#>]";
    165:                  List<<#=Cleans(tbl.CleanName)#>>  list =new List<<#=Cleans(tbl.CleanName)#>>();
    166:                  using(IDataReader reader = base.ExcuteDataReader(sql))
    167:                  {
    168:                      while(reader.Read())
    169:                      {
    170:                          <#=Cleans(tbl.CleanName)#> item =new <#=Cleans(tbl.CleanName)#>();
    171:                          <#
    172:                      i=0;
    173:                      foreach(var col in tbl.Columns){
    174:                              if(col.IsNullable)
    175:                              {
    176:                         #>if(!reader.IsDBNull(<#=i#>))
    177:                           {
    178:                              item.<#= col.CleanName #> = <#=GetReadFormatString("reader.",col.DataType,i)  #>;
    179:                           }
    180:                           <# }
    181:                              else
    182:                              {
    183:                                  #>
    184:  item.<#= col.CleanName #> = <#=GetReadFormatString("reader.",col.DataType,i)  #>;
    185:                              <#
    186:                              }                        
    187:                          i++;
    188:                      }#>
    189:                          list.Add(item);
    190:                      }
    191:                  }
    192:                  return list;
    193:              }
    194:  
    195:          }
    196:          
    197:  <#    
    198:          }
    199:          
    200:      }
    201:  #>
    202:  }

    而Structs.tt 比较简单还是根据表数据生成实体,这里可以扩展比如你要在实体上加上验证。

     1:  <#@ template language="C#" debug="False" hostspecific="True"  #>
     2:  <#@ output extension=".cs" #>
     3:  <#@ include file="SQLServer.ttinclude" #>
     4:  <#
     5:  var tables = LoadTables();
     6:  #>
     7:  //=============================================
     8:  // 该
     9:  // 生 <#= DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") #>
    10:  // =============================================
    11:  using System;
    12:  using System.Collections.Generic;
    13:  using System.Data;
    14:  
    15:  namespace <#=Namespace#> {    
    16:  <#  foreach(var tbl in tables){
    17:          if(!ExcludeTables.Contains(tbl.Name))
    18:          {
    19:  #>
    20:          /// <summary>
    21:          /// Table: <#=tbl.Name#>
    22:          /// Primary Key: <#=tbl.PrimaryKey#>
    23:          /// <#=string.IsNullOrEmpty(tbl.Description)?tbl.Name:tbl.Description.Replace("\r\n", "\r\n            ///")#>
    24:          /// </summary>
    25:          public partial class <#=Cleans(tbl.CleanName)#>:BaseEntity {  
    26:              
    27:  <#          foreach(var col in tbl.Columns){#>
    28:              private <#=col.SysType#> _<#=col.CleanName#>;
    29:              /// <summary>
    30:              ///  <#=string.IsNullOrEmpty(col.Description)?col.Name:col.Description.Replace("\r\n", "\r\n            ///")#>
    31:              /// </summary>
    32:              public <#=col.SysType#> <#=col.CleanName#>{
    33:                  get{
    34:                      return _<#=col.CleanName#>;
    35:                  }
    36:                  set
    37:                  {
    38:                      _<#=col.CleanName#>= value;
    39:                      OnPropertyChanged("<#=col.CleanName#>");
    40:                  }
    41:              }
    42:  <#   }#>                
    43:          }
    44:          
    45:  <#}}#>
    46:  }

    StoredProcedures.tt 则是生成对存储过程的封装,不要再去记那繁琐的存储过程名字,参数名字,参数类型了。

     1:  <#@ template language="C#v3.5" debug="False" hostspecific="True"  #>
     2:  <#@ output extension=".cs" #>
     3:  <#@ include file="SQLServer.ttinclude" #>
     4:  <#
     5:      var sps = GetSPs(); 
     6:      if(sps.Count>0){ 
     7:  #>  
     8:  //=============================================
     9:  // 该
    10:  // 生 <#= DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") #>
    11:  // =============================================
    12:  using System;
    13:  using System.Data;
    14:  
    15:  namespace <#=Namespace#>{
    16:      public partial class StoredProcedures{
    17:  
    18:  <#  foreach(var sp in sps){#>
    19:          public static StoredProcedure <#=sp.CleanName#>(<#=sp.ArgList#>){
    20:              StoredProcedure sp=new StoredProcedure("<#=sp.Name#>");
    21:  <#      foreach(var par in sp.Parameters){#>
    22:              sp.AddParameter("<#=par.Name#>",<#=par.CleanName#>,DbType.<#=par.DbType#>);
    23:  <#      }#>
    24:              return sp;
    25:          }
    26:  <#  }#>
    27:      
    28:      }
    29:      
    30:  }
    31:  <#  }#> 

    有了这些至少最基本的代码不用编写,同时在编写有些代码的时候我们可以复制了。。

    尽管如此,其实在代码中我大多的数据访问代码除了实体和存储过程调用,其他的我还是自己写sql的,哈哈。。。。

    2 使用 StructureMap 来实现IOC

    上面已经给出了StructureMap ,以前也用过Castle,不过我发现这个就一个DLL,而且使用起来很简洁方便。性能上也还不错

    在控制器中通过构造函数引入服务

     image

    定义服务的接口

    image

    实现服务,如果服务的实现又依赖其他服务可以通过同样的方式引入

    image

    然后注册服务,我这里是通过代码进行映射的,也可以通过配置文件等方法,同时也可以通过配置文件传递构造函数的参数等,具体请看

    http://structuremap.net/structuremap/

    声明一个注册器,根据不能服务类型可以声明不同的注册器,这里我只声明了一个

     1:    public class ServiceRegistry : Registry
     2:      {
     3:          public ServiceRegistry()
     4:          {
     5:              For<ISysManageService>().Use<SysManageService>();
     6:              For<IUserService>().Use<UserService>();
     7:              For<ILogService>().Use<LogService>();
     8:          }
     9:         
    10:      }
    11:  

    然后是注册注册器的包装了,如果有多个注册器,这里则需要初始化多个注册器

     1:    public static class Bootstrapper
     2:      {
     3:          public static void ConfigureStructureMap()
     4:          {
     5:              ObjectFactory.Initialize(x => { 
     6:                  x.AddRegistry(new Core.ServiceRegistry()); 
     7:              });         
     8:          }
     9:      }
    10:  

    接着我们要重新实现以下MVC的控制器工厂类

        public class StructureMapControllerFactory : DefaultControllerFactory
        {
            protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
            {
                if (controllerType == null)
                {
                    throw new HttpException(404,
                        String.Format(
                            "请求的地址{0}不存在",
                            requestContext.HttpContext.Request.Path));
                }
                if (!typeof(IController).IsAssignableFrom(controllerType))
                {
                    throw new ArgumentException(
                        String.Format(                     
                           "没有合适的控制器实现{0}",
                            controllerType),
                        "controllerType");
                }
                try
                {
                   
                    return ObjectFactory.GetInstance(controllerType) as Controller;
                }
                catch (StructureMapException)
                {
                    System.Diagnostics.Debug.WriteLine(ObjectFactory.WhatDoIHave());
                    throw;
                }
            }
          
        }

    最后需要注册以下,在网站的Global.asax 中

      protected void Application_Start()
            {
                AreaRegistration.RegisterAllAreas();            
               
                RegisterGlobalFilters(GlobalFilters.Filters);
                RegisterRoutes(RouteTable.Routes);
    
    
                //Configure StructureMapConfiguration
                Bootstrapper.ConfigureStructureMap();
                //Set current Controller factory as StructureMapControllerFactory
                ControllerBuilder.Current.SetControllerFactory(new xEasyApp.Core.StructureMapControllerFactory()); 
            }
    

    3 编写json实体用于和客户端交互

    因为xEasyApp多数的请求都是ajax访问的定义json提示非常又必要,不然自己去拼接json字符串,容易出错,也不好维护。其实大家就可以通过实体看出json数据格式了。也便于大家理解

    比较复杂就Flexigrid的数据实体了 但是编写成服务端实体就看着不麻烦

     public class JsonFlexiGridData
        {
            public JsonFlexiGridData()
            {
            }
    
            public JsonFlexiGridData(
                int pageIndex, int totalCount, IList<FlexiGridRow> data)
            {
                page = pageIndex;
                total = totalCount;
                rows = data;
            }
            public int page { get; set; }
            public int total { get; set; }
            public IList<FlexiGridRow> rows { get; set; }
            /// <summary>
            /// Gets or sets the error.
            /// </summary>
            /// <value>The error.</value>
            public FlexiGridError error { get; set; }
    
            public static JsonFlexiGridData ConvertFromList<T>(List<T> list,string key,string[] cols) where T:class
            {
                JsonFlexiGridData data = new JsonFlexiGridData();
                data.page = 1;
                if (list != null)
                {
                    data.total = list.Count;
                    data.rows = new List<FlexiGridRow>();
                    foreach (T t in list)
                    { 
                        FlexiGridRow row =new FlexiGridRow();
                        row.id = getValue<T>(t,key);
                        row.cell = new List<string>();
                        foreach(string col in cols)
                        {
                            row.cell.Add(getValue<T>(t, col));
                        }
                        data.rows.Add(row);
                    }
                }
                else
                {
                    data.total = 0;
                }
                return data;
            }
    
            private static string getValue<T>(T t,string pname) where T:class
            {
                Type type = t.GetType();
                PropertyInfo pinfo = type.GetProperty(pname);
                if (pinfo != null)
                {
                    object v = pinfo.GetValue(t, null);
                    return v != null ? v.ToString() : "";
                }
                return "";
            }
            public static JsonFlexiGridData ConvertFromPagedList<T>(PagedList<T> pagelist, string key, string[] cols) where T : class
            {
                JsonFlexiGridData data = new JsonFlexiGridData();
                data.page = pagelist.PageIndex+1;
                if (pagelist.PageIndex == 0)
                {
                    data.total = pagelist.Total;
                }
                data.rows = new List<FlexiGridRow>();
                foreach (T t in pagelist.DataList)
                {
                    FlexiGridRow row = new FlexiGridRow();
                    row.id = getValue<T>(t, key);
                    row.cell = new List<string>();
                    foreach (string col in cols)
                    {
                        row.cell.Add(getValue<T>(t, col));
                    }
                    data.rows.Add(row);
                }
                return data;
            }
        }
    
      /// <summary>
        /// flexigrid的数据行
        /// </summary>
        public class FlexiGridRow
        {
            public string id { get; set; }
            public List<string> cell { get; set; }
        }
    这里定义了两个比较有用的方法,一个是List转换成flexigrid .另外一个则是通过PageList转换成flexigrid的数据
    你只需提供主键列和列信息(而这两个 已经由前端发送到服务端了哦) 。这样可以避免在写查询的时候也要和前端的列定义一一对应和编写繁琐的转换代码了
     
    生成的flexigrid格式是大概这样的
    {
    	"page" : 2, //页码
    	"total" : -1, //总记录数,flexigrid我修改为只在第一页获取总记录数
    	"rows" : [{
    			"id" : "26", //行主键
    			"cell" : ["26", "棉花糖", "每箱30盒", "31.2300", "15", "0", "26"] //每行的数据 ,必须和前端定义的列一一对应,顺序必须一致
    		}, {
    			"id" : "27",
    			"cell" : ["27", "牛肉干", "每箱30包", "43.9000", "49", "0", "27"] 
    		}
    	]
    }
     
    另一个常用的则是TreeNode之前竟然有人来问为什么一切都是对的,但是前端就是没有显示数据,多半是因为服务端json数据格式不正确或者属性的值设置不正确导致的
     public class JsonTreeNode
        {
            #region properties
    
            /// <summary>
            /// treenode的主键必须唯一
            /// </summary>
            /// <value>The id.</value>
            public string id { get; set; }
    
            /// <summary>
            /// treenode的显示文本
            /// </summary>
            /// <value>The text.</value>
            public string text { get; set; }
    
            /// <summary>
            /// 节点的值
            /// </summary>
            /// <value>The value.</value>
            public string value { get; set; }
    
            /// <summary>
            /// 是否显示checkbox,如果前端设置了配置 showcheck: true,这里设置为true也有用,否则始终不会显示checkbox
            /// 相反就算你前端设置了showcheck: true ,这个属性设置false那么前端这个节点也不会显示checkbox。
            /// </summary>
            /// <value><c>true</c> if showcheck; otherwise, <c>false</c>.</value>
            public bool showcheck { get; set; }
    
            /// <summary>
            /// 是否展开,一般用于父节点展开,同时再获取一下节点的数据。
            /// 当设置为true时应该保证他的下级数据已经加载。
            /// </summary>
            /// <value><c>true</c> if isexpand; otherwise, <c>false</c>.</value>
            public bool isexpand { get; set; }
    
            /// <summary>
            /// 选中的状态0,1,2 0为没有选中,1为选中,2为半选
            /// </summary>
            /// <value>The checkstate.</value>
            public byte checkstate { get; set; }
    
            /// <summary>
            /// 是否有子节点
            /// </summary>
            /// <value>
            /// 	<c>true</c> if this instance has children; otherwise, <c>false</c>.
            /// </value>
            public bool hasChildren { get; set; }
    
            /// <summary>
            /// 是否已经完成加载,如果这个节点设置为true则,展开节点时不会发起ajax请求。
            /// </summary>
            /// <value><c>true</c> if complete; otherwise, <c>false</c>.</value>
            public bool complete { get; set; }
    
            /// <summary>
            /// 节点的自定义样式,可以为节点设置特定的样式,如修改节点的图标
            /// </summary>
            /// <value>The classes.</value>
            public string classes { get; set; }
    
            /// <summary>
            /// 额外的数据
            /// </summary>
            /// <value>The data.</value>
            public Dictionary<string, string> data { get; set; }
    
            private List<JsonTreeNode> _ChildNodes;
            public List<JsonTreeNode> ChildNodes
            {
                get
                {
                    if (_ChildNodes == null)
                    {
                        _ChildNodes = new List<JsonTreeNode>();
                    }
                    return _ChildNodes;
                }
            }
    
            #endregion
        }

    我在代码中加入了详细的说明,大家可以参考下。。另外几个都是比较简单的如一般操作的返回成功失败的消息定义,就不一一介绍了

    在MVC中只需 image  这样就可以了方便吧

     

    下节,我讲介绍客户端的调用和注意事项,并且描述下几个常用的场景和我的思考,另外介绍下最近的一些更新。。

    你的支持是我继续写作的动力

  • 相关阅读:
    String内置方法
    【练习题】三级城市选择
    【练习题】购物车练习
    【练习题】计算还能活多少年
    【练习题】猜年龄
    【练习题】比大小
    【练习题】打印长方形
    【练习题】格式化打印
    【练习题】奇数偶数打印
    Ansible配置管理工具
  • 原文地址:https://www.cnblogs.com/xuanye/p/2095085.html
Copyright © 2011-2022 走看看