引言
昨天加了一天班,今天闲来无事,就在想如何将之前的三层和最近一直在学的设计模式给联系在一起,然后就动手弄了个下面的小demo。
项目结构
项目各个层实现
Wolfy.Model层中有一个抽象类BaseModel.cs,User.cs是用户实体类,继承与BaseModel类,是用于类型安全考虑的,让各实体类有个统一的父类,在其他层使用的时候,可以使用里氏替换原则的考虑。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Wolfy.Model 8 { 9 /// <summary> 10 /// 该抽象类为所有实体类的父类, 11 /// 所有实体类继承该抽象类, 为保持类型一致而设计的父类,也是出于安全性的考虑 12 /// </summary> 13 public abstract class BaseModel 14 { 15 } 16 }
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Wolfy.Model 8 { 9 /// <summary> 10 /// 实体类user继承自BaseModel 11 /// 调用时就可以通过BaseModel model=new UserModel(); 12 /// </summary> 13 public class UserModel : BaseModel 14 { 15 public int Id { get; set; } 16 public string UserName { set; get; } 17 public string Password { set; get; } 18 } 19 }
Wolfy.FactoryDAL层是用于反射获取实例,其中只有一个类。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Wolfy.FactoryDAL 8 { 9 public class DataAccess<T> where T : class 10 { 11 //获取配置路径 12 private static readonly string path = System.Configuration.ConfigurationManager.AppSettings["DAL"]; 13 private DataAccess() { } 14 /// <summary> 15 /// 创建实例 反射创建实例 16 /// </summary> 17 /// <param name="type"></param> 18 /// <returns></returns> 19 public static T CreateDAL(string type) 20 { 21 string className = string.Format(path + ".{0}", type); 22 try 23 { 24 return (T)System.Reflection.Assembly.Load(path).CreateInstance(className); 25 26 } 27 catch (Exception ex) 28 { 29 throw new Exception(ex.Message.ToString()); 30 } 31 } 32 } 33 }
Wolfy.IDAL层依赖与Wolfy.Model,其中包含一个基接口IBaseDAL.cs,还有一个用于定义一些基接口中没有方法的接口IUserDAL,继承基接口IBaseDAL<T>
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Wolfy.IDAL 8 { 9 /// <summary> 10 /// 所有的dal基本都有增删改查等功能,提取到dal接口层, 11 /// 所有实现该接口的类必须实现所有的未实现的成员 12 /// </summary> 13 /// <typeparam name="T"></typeparam> 14 public interface IBaseDAL<T> where T : Model.BaseModel, new() 15 { 16 bool Add(T model); 17 bool Detele(int ID); 18 bool Update(T model); 19 T GetModel(int ID); 20 } 21 }
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Wolfy.IDAL 8 { 9 /// <summary> 10 /// 需有特殊的方法的 可定义子接口 11 /// </summary> 12 public interface IUserDAL:IBaseDAL<Model.UserModel> 13 { 14 /// <summary> 15 /// 判断用户名是否存在 16 /// </summary> 17 /// <param name="userName"></param> 18 /// <returns></returns> 19 bool Exists(string userName); 20 /// <summary> 21 /// 登录 22 /// </summary> 23 /// <param name="name"></param> 24 /// <param name="pwd"></param> 25 /// <returns></returns> 26 Model.UserModel Login(string name, string pwd); 27 } 28 }
Wolfy.DAL层,处理数据库的操作。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Data; 7 using System.Data.SqlClient; 8 namespace Wolfy.DAL 9 { 10 public class UserDAL : Wolfy.IDAL.IUserDAL 11 { 12 public bool Exists(string userName) 13 { 14 string sql = "select count(*) from Users where UserName=@UserName"; 15 SqlParameter[] sp = { 16 new SqlParameter("@UserName",userName) 17 }; 18 19 return (int)SqlHelper.ExecuteScalar(CommandType.Text, sql, sp) > 0; 20 } 21 22 public bool Add(Model.UserModel model) 23 { 24 string sql = "insert into Users values(@UserName,@UserPwd)"; 25 SqlParameter[] sp = { 26 new SqlParameter("@UserName",model.UserName), 27 new SqlParameter("@UserName",model.Password) 28 }; 29 return SqlHelper.ExecuteNonQuery(CommandType.Text, sql, sp) > 0; 30 } 31 32 public bool Detele(int ID) 33 { 34 string sql = "delete from Users where id=" + ID; 35 return SqlHelper.ExecuteNonQuery(CommandType.Text, sql) > 0; 36 } 37 38 public bool Update(Model.UserModel model) 39 { 40 string sql = string.Format("update Users set UserName={0},UserPwd={1} where id={2}", model.UserName, model.Password, model.Id); 41 return SqlHelper.ExecuteNonQuery(CommandType.Text, sql) > 0; 42 } 43 44 public Model.UserModel GetModel(int ID) 45 { 46 string sql = "select * from Users where id=" + ID; 47 DataTable dt = SqlHelper.ExecuteDataTable(sql); 48 if (dt != null && dt.Rows.Count > 0) 49 { 50 return new Model.UserModel() { UserName = dt.Rows[0]["UserName"].ToString(), Password = dt.Rows[0]["UserPwd"].ToString() }; 51 } 52 else 53 { 54 return null; 55 } 56 } 57 58 59 public Model.UserModel Login(string name, string pwd) 60 { 61 Model.UserModel model = null; 62 string sql = "select * from Users where UserName=@UserName and UserPwd=@UserPwd"; 63 SqlParameter[] sp = { 64 new SqlParameter("@UserName",name), 65 new SqlParameter("@UserPwd",pwd) 66 }; 67 SqlDataReader reader = SqlHelper.ExecuteReader(CommandType.Text, sql, sp); 68 if (reader != null && !reader.IsClosed && reader.HasRows) 69 { 70 model = new Model.UserModel(); 71 while (reader.Read()) 72 { 73 model.Id = Convert.ToInt32(reader[0]); 74 model.UserName = reader[1].ToString(); 75 model.Password = reader[2].ToString(); 76 } 77 } 78 reader.Dispose(); 79 return model; 80 } 81 } 82 }
1 using System; 2 using System.Data; 3 using System.Xml; 4 using System.Data.SqlClient; 5 using System.Collections; 6 using System.Configuration; 7 using System.IO; 8 using System.Web; 9 10 11 namespace Wolfy.DAL 12 { 13 /// <summary> 14 /// 数据库的通用访问代码 15 /// 此类为抽象类,不允许实例化,在应用时直接调用即可 16 /// </summary> 17 public abstract class SqlHelper 18 { 19 //获取数据库连接字符串,其属于静态变量且只读,项目中所有文档可以直接使用,但不能修改 20 public static readonly string connectionString = ConfigurationManager.ConnectionStrings["SqlConnect"].ConnectionString; 21 22 23 // 哈希表用来存储缓存的参数信息,哈希表可以存储任意类型的参数。 24 private static Hashtable parmCache = Hashtable.Synchronized(new Hashtable()); 25 26 /// <summary> 27 ///执行一个不需要返回值的SqlCommand命令,通过指定专用的连接字符串。 28 /// 使用参数数组形式提供参数列表 29 /// </summary> 30 /// <remarks> 31 /// 使用示例: 32 /// int result = ExecuteNonQuery(connString, CommandType.StoredProcedure, "PublishOrders", new SqlParameter("@prodid", 24)); 33 /// </remarks> 34 /// <param name="connectionString">一个有效的数据库连接字符串</param> 35 /// <param name="commandType">SqlCommand命令类型 (存储过程, T-SQL语句, 等等。)</param> 36 /// <param name="commandText">存储过程的名字或者 T-SQL 语句</param> 37 /// <param name="commandParameters">以数组形式提供SqlCommand命令中用到的参数列表</param> 38 /// <returns>返回一个数值表示此SqlCommand命令执行后影响的行数</returns> 39 public static int ExecuteNonQuery(CommandType cmdType, string cmdText, params SqlParameter[] commandParameters) 40 { 41 SqlCommand cmd = new SqlCommand(); 42 using (SqlConnection conn = new SqlConnection(connectionString)) 43 { 44 //通过PrePareCommand方法将参数逐个加入到SqlCommand的参数集合中 45 PrepareCommand(cmd, conn, null, cmdType, cmdText, commandParameters); 46 int val = cmd.ExecuteNonQuery(); 47 //清空SqlCommand中的参数列表 48 cmd.Parameters.Clear(); 49 return val; 50 } 51 } 52 53 /// <summary> 54 ///执行一条不返回结果的SqlCommand,通过一个已经存在的数据库连接 55 /// 使用参数数组提供参数 56 /// </summary> 57 /// <remarks> 58 /// 使用示例: 59 /// int result = ExecuteNonQuery(conn, CommandType.StoredProcedure, "PublishOrders", new SqlParameter("@prodid", 24)); 60 /// </remarks> 61 /// <param name="conn">一个现有的数据库连接</param> 62 /// <param name="commandType">SqlCommand命令类型 (存储过程, T-SQL语句, 等等。)</param> 63 /// <param name="commandText">存储过程的名字或者 T-SQL 语句</param> 64 /// <param name="commandParameters">以数组形式提供SqlCommand命令中用到的参数列表</param> 65 /// <returns>返回一个数值表示此SqlCommand命令执行后影响的行数</returns> 66 public static int ExecuteNonQuery(SqlConnection connection, CommandType cmdType, string cmdText, params SqlParameter[] commandParameters) 67 { 68 SqlCommand cmd = new SqlCommand(); 69 PrepareCommand(cmd, connection, null, cmdType, cmdText, commandParameters); 70 int val = cmd.ExecuteNonQuery(); 71 cmd.Parameters.Clear(); 72 return val; 73 } 74 75 /// <summary> 76 /// 执行一条不返回结果的SqlCommand,通过一个已经存在的数据库事物处理 77 /// 使用参数数组提供参数 78 /// </summary> 79 /// <remarks> 80 /// 使用示例: 81 /// int result = ExecuteNonQuery(trans, CommandType.StoredProcedure, "PublishOrders", new SqlParameter("@prodid", 24)); 82 /// </remarks> 83 /// <param name="trans">一个存在的 sql 事物处理</param> 84 /// <param name="commandType">SqlCommand命令类型 (存储过程, T-SQL语句, 等等。)</param> 85 /// <param name="commandText">存储过程的名字或者 T-SQL 语句</param> 86 /// <param name="commandParameters">以数组形式提供SqlCommand命令中用到的参数列表</param> 87 /// <returns>返回一个数值表示此SqlCommand命令执行后影响的行数</returns> 88 public static int ExecuteNonQuery(SqlTransaction trans, CommandType cmdType, string cmdText, params SqlParameter[] commandParameters) 89 { 90 SqlCommand cmd = new SqlCommand(); 91 PrepareCommand(cmd, trans.Connection, trans, cmdType, cmdText, commandParameters); 92 int val = cmd.ExecuteNonQuery(); 93 cmd.Parameters.Clear(); 94 return val; 95 } 96 97 /// <summary> 98 /// 执行一条返回结果集的SqlCommand命令,通过专用的连接字符串。 99 /// 使用参数数组提供参数 100 /// </summary> 101 /// <remarks> 102 /// 使用示例: 103 /// SqlDataReader r = ExecuteReader(connString, CommandType.StoredProcedure, "PublishOrders", new SqlParameter("@prodid", 24)); 104 /// </remarks> 105 /// <param name="connectionString">一个有效的数据库连接字符串</param> 106 /// <param name="commandType">SqlCommand命令类型 (存储过程, T-SQL语句, 等等。)</param> 107 /// <param name="commandText">存储过程的名字或者 T-SQL 语句</param> 108 /// <param name="commandParameters">以数组形式提供SqlCommand命令中用到的参数列表</param> 109 /// <returns>返回一个包含结果的SqlDataReader</returns> 110 public static SqlDataReader ExecuteReader(CommandType cmdType, string cmdText, params SqlParameter[] commandParameters) 111 { 112 SqlCommand cmd = new SqlCommand(); 113 SqlConnection conn = new SqlConnection(connectionString); 114 // 在这里使用try/catch处理是因为如果方法出现异常,则SqlDataReader就不存在, 115 //CommandBehavior.CloseConnection的语句就不会执行,触发的异常由catch捕获。 116 //关闭数据库连接,并通过throw再次引发捕捉到的异常。 117 try 118 { 119 PrepareCommand(cmd, conn, null, cmdType, cmdText, commandParameters); 120 SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection); 121 cmd.Parameters.Clear(); 122 return rdr; 123 } 124 catch 125 { 126 conn.Close(); 127 throw; 128 } 129 } 130 131 /// <summary> 132 /// 执行一条返回第一条记录第一列的SqlCommand命令,通过专用的连接字符串。 133 /// 使用参数数组提供参数 134 /// </summary> 135 /// <remarks> 136 /// 使用示例: 137 /// Object obj = ExecuteScalar(connString, CommandType.StoredProcedure, "PublishOrders", new SqlParameter("@prodid", 24)); 138 /// </remarks> 139 /// <param name="connectionString">一个有效的数据库连接字符串</param> 140 /// <param name="commandType">SqlCommand命令类型 (存储过程, T-SQL语句, 等等。)</param> 141 /// <param name="commandText">存储过程的名字或者 T-SQL 语句</param> 142 /// <param name="commandParameters">以数组形式提供SqlCommand命令中用到的参数列表</param> 143 /// <returns>返回一个object类型的数据,可以通过 Convert.To{Type}方法转换类型</returns> 144 public static object ExecuteScalar(CommandType cmdType, string cmdText, params SqlParameter[] commandParameters) 145 { 146 SqlCommand cmd = new SqlCommand(); 147 using (SqlConnection connection = new SqlConnection(connectionString)) 148 { 149 PrepareCommand(cmd, connection, null, cmdType, cmdText, commandParameters); 150 object val = cmd.ExecuteScalar(); 151 cmd.Parameters.Clear(); 152 return val; 153 } 154 } 155 156 /// <summary> 157 /// 执行一条返回第一条记录第一列的SqlCommand命令,通过已经存在的数据库连接。 158 /// 使用参数数组提供参数 159 /// </summary> 160 /// <remarks> 161 /// 使用示例: 162 /// Object obj = ExecuteScalar(connString, CommandType.StoredProcedure, "PublishOrders", new SqlParameter("@prodid", 24)); 163 /// </remarks> 164 /// <param name="conn">一个已经存在的数据库连接</param> 165 /// <param name="commandType">SqlCommand命令类型 (存储过程, T-SQL语句, 等等。)</param> 166 /// <param name="commandText">存储过程的名字或者 T-SQL 语句</param> 167 /// <param name="commandParameters">以数组形式提供SqlCommand命令中用到的参数列表</param> 168 /// <returns>返回一个object类型的数据,可以通过 Convert.To{Type}方法转换类型</returns> 169 public static object ExecuteScalar(SqlConnection connection, CommandType cmdType, string cmdText, params SqlParameter[] commandParameters) 170 { 171 SqlCommand cmd = new SqlCommand(); 172 PrepareCommand(cmd, connection, null, cmdType, cmdText, commandParameters); 173 object val = cmd.ExecuteScalar(); 174 cmd.Parameters.Clear(); 175 return val; 176 } 177 178 /// <summary> 179 /// 缓存参数数组 180 /// </summary> 181 /// <param name="cacheKey">参数缓存的键值</param> 182 /// <param name="cmdParms">被缓存的参数列表</param> 183 public static void CacheParameters(string cacheKey, params SqlParameter[] commandParameters) 184 { 185 parmCache[cacheKey] = commandParameters; 186 } 187 188 /// <summary> 189 /// 获取被缓存的参数 190 /// </summary> 191 /// <param name="cacheKey">用于查找参数的KEY值</param> 192 /// <returns>返回缓存的参数数组</returns> 193 public static SqlParameter[] GetCachedParameters(string cacheKey) 194 { 195 SqlParameter[] cachedParms = (SqlParameter[])parmCache[cacheKey]; 196 if (cachedParms == null) 197 return null; 198 //新建一个参数的克隆列表 199 SqlParameter[] clonedParms = new SqlParameter[cachedParms.Length]; 200 //通过循环为克隆参数列表赋值 201 for (int i = 0, j = cachedParms.Length; i < j; i++) 202 //使用clone方法复制参数列表中的参数 203 clonedParms[i] = (SqlParameter)((ICloneable)cachedParms[i]).Clone(); 204 return clonedParms; 205 } 206 207 /// <summary> 208 /// 为执行命令准备参数 209 /// </summary> 210 /// <param name="cmd">SqlCommand 命令</param> 211 /// <param name="conn">已经存在的数据库连接</param> 212 /// <param name="trans">数据库事物处理</param> 213 /// <param name="cmdType">SqlCommand命令类型 (存储过程, T-SQL语句, 等等。)</param> 214 /// <param name="cmdText">Command text,T-SQL语句 例如 Select * from Products</param> 215 /// <param name="cmdParms">返回带参数的命令</param> 216 private static void PrepareCommand(SqlCommand cmd, SqlConnection conn, SqlTransaction trans, CommandType cmdType, string cmdText, SqlParameter[] cmdParms) 217 { 218 //判断数据库连接状态 219 if (conn.State != ConnectionState.Open) 220 conn.Open(); 221 cmd.Connection = conn; 222 cmd.CommandText = cmdText; 223 //判断是否需要事物处理 224 if (trans != null) 225 cmd.Transaction = trans; 226 cmd.CommandType = cmdType; 227 if (cmdParms != null) 228 { 229 foreach (SqlParameter parm in cmdParms) 230 cmd.Parameters.Add(parm); 231 } 232 } 233 234 /// <summary> 235 /// 获取dataset数据 236 /// </summary> 237 /// <param name="sql"></param> 238 /// <returns></returns> 239 public static DataTable ExecuteDataTable(string sql) 240 { 241 using (SqlConnection conn = new SqlConnection(connectionString)) 242 { 243 SqlDataAdapter da = new SqlDataAdapter(sql, conn); 244 DataTable dst = new DataTable(); 245 da.Fill(dst); 246 return dst; 247 } 248 } 249 } 250 }
Wolfy.BLL业务逻辑层中包含了一个用于继承的基类BaseBLL<T>和用户业务逻辑UserBLL类,这层依赖Wolfy.IDAL,Wolfy.Model,Wolfy.FactoryDAL库
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Wolfy.BLL 8 { 9 public class BaseBLL<T> where T : Wolfy.Model.UserModel, new() 10 { 11 protected Wolfy.IDAL.IBaseDAL<T> Dal; 12 public BaseBLL(string type) 13 { 14 //通过工厂得到 dal 15 Dal = Wolfy.FactoryDAL.DataAccess<Wolfy.IDAL.IBaseDAL<T>>.CreateDAL(type); 16 } 17 public virtual bool Add(T model) 18 { 19 return Dal.Add(model); 20 } 21 public virtual bool Delete(int ID) 22 { return Dal.Detele(ID); } 23 public virtual bool Update(T model) 24 { return Dal.Update(model); } 25 public virtual T GetModel(int ID) 26 { 27 return Dal.GetModel(ID); 28 } 29 30 } 31 }
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Wolfy.BLL 8 { 9 public class UserBLL : BaseBLL<Wolfy.Model.UserModel> 10 { 11 private const string _Type = "UserDAL"; 12 private Wolfy.IDAL.IUserDAL _DAL; 13 public UserBLL() 14 : base(_Type) 15 { 16 _DAL = base.Dal as Wolfy.IDAL.IUserDAL; 17 if (_DAL == null) 18 { 19 throw new NullReferenceException(_Type); 20 } 21 } 22 public bool Exists(string userName) 23 { 24 return _DAL.Exists(userName); 25 } 26 public Model.UserModel Login(string name, string pwd) 27 { return _DAL.Login(name, pwd); } 28 } 29 }
web.config程序集名称,连接字符串配置
1 <?xml version="1.0" encoding="utf-8"?> 2 <!-- 3 有关如何配置 ASP.NET 应用程序的详细信息,请访问 4 http://go.microsoft.com/fwlink/?LinkId=169433 5 --> 6 <configuration> 7 <system.web> 8 <compilation debug="true" targetFramework="4.5" /> 9 <httpRuntime targetFramework="4.5" /> 10 11 </system.web> 12 <connectionStrings> 13 <add name="SqlConnect" connectionString="server=.;database=Test;uid=sa;pwd=sa"/> 14 </connectionStrings> 15 <appSettings> 16 <add key="DAL" value="Wolfy.DAL"/> 17 </appSettings> 18 </configuration>
测试
简单的ajax登录,web项目需引用Wolfy.BLL.dll和Wolfy.Model.dll和Wolfy.DAL.dll
1 <!DOCTYPE html> 2 <html xmlns="http://www.w3.org/1999/xhtml"> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 5 <title>wolfy信息系统登录</title> 6 <script type="text/javascript" src="Scripts/jquery-1.11.0.js"></script> 7 <script type="text/javascript"> 8 $(function () { 9 $("#btnLogin").click(function () { 10 var name = $("#txtUserName").val(); 11 var pwd = $("#txtPwd").val(); 12 $.ajax({ 13 url: "Ashx/Login.ashx", 14 data: "name=" + name + "&pwd=" + pwd, 15 type: "Post", 16 dataType: "text", 17 success: function (msg) { 18 if (msg == "1") { 19 $("#divMsg").html("登录成功"); 20 } else if(msg=="2") { 21 $("#divMsg").html("用户名或密码为空"); 22 } else if(msg=="3"){ 23 $("#divMsg").html("用户名不存在"); 24 } else { 25 $("#divMsg").html("密码错误"); 26 } 27 } 28 29 30 }); 31 }); 32 }); 33 </script> 34 </head> 35 <body> 36 <table> 37 <tr> 38 <td>用户名:</td> 39 <td><input type="text" id="txtUserName" name="name" value="admin" /></td> 40 </tr> 41 <tr> 42 <td>密码:</td> 43 <td><input type="password" id="txtPwd" name="name" value="admin" /></td> 44 </tr> 45 <tr> 46 <td colspan="2"><input type="button" id="btnLogin" name="name" value="登录" /></td> 47 </tr> 48 </table> 49 <div id="divMsg"></div> 50 </body> 51 </html>
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Web; 5 6 namespace Wolfy.LoginDemo.Ashx 7 { 8 /// <summary> 9 /// Login 的摘要说明 10 /// </summary> 11 public class Login : IHttpHandler 12 { 13 14 public void ProcessRequest(HttpContext context) 15 { 16 context.Response.ContentType = "text/plain"; 17 //接收用户名和密码 18 string name = context.Request["name"]; 19 string pwd = context.Request["pwd"]; 20 if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(pwd)) 21 { 22 context.Response.Write("2"); 23 } 24 else 25 { 26 BLL.UserBLL bll = new BLL.UserBLL(); 27 Model.UserModel model = new Model.UserModel(); 28 if (!bll.Exists(name)) 29 { 30 context.Response.Write("3"); 31 } 32 else 33 { 34 model = bll.Login(name, pwd); 35 if (model != null) 36 { 37 //登录成功记入cookie 38 context.Response.Cookies["n"].Value = name; 39 context.Response.Cookies["n"].Expires = DateTime.Now.AddDays(7); 40 context.Response.Cookies["p"].Value = pwd; 41 context.Response.Cookies["p"].Expires = DateTime.Now.AddDays(7); 42 context.Response.Write("1"); 43 } 44 } 45 46 47 48 } 49 } 50 51 public bool IsReusable 52 { 53 get 54 { 55 return false; 56 } 57 } 58 } 59 }
结果