本來關於T4模板原想分5個章節詳細解說的,不過由於最近比較忙,也不想將整個系列時間拉得太長,因此就將它們整合在一塊了,可能會有不少細節沒有講到,但願你們本身對着代碼與模板去研究。前端
本章代碼量會比較大,基本將Web層要使用到的大部分函數都用模板生成了出來,而模板中的函數,不少也是互相關聯調用的。另外在DotNet.Utilities(公共函數項目)中也添加與修改了一些類和函數。數據庫
須要特別說明的是,在邏輯層添加了July大神編寫的超強上傳類,具體怎麼使用功能怎麼強大,在後面調用到時會用一個章節詳細說明。呵呵......後端
一、先了解解決方案中各個新增文件功能,具體的文件對應說明,請查看《數據字典》中的「目錄與文件說明」緩存
這個是各個表對應的邏輯層類,裏面是Web層經常使用的各類函數。Application文件夾是各類公共邏輯層函數類,Systems文件夾是後端系統管理模塊經常使用函數類。之後添加新的文件時,能夠按功能或業務進行對應分類存放。併發
LogicBase.cs是邏輯層全部模板生成類的父類,裏面有兩個虛函數,用來給模板類調用。在有須要的時候,在自定義類中重寫這兩個函數,就能夠給模板中的相應函數自動調用執行。框架
DelCache()函數是模板類中進行添加、修改、刪除、更新等對數據庫記錄進行變動時會同步調用到,主要用於擁有自定義緩存的類中,重寫該函數後,進行前面的各項操做時自動執行該函數,以達到同步理清自定義緩存的功能。ide
GetExpression()函數是提供給模板類中的緩存加載函數(GetList())使用的。咱們在開發時會建立不少表,有些表所有記錄須要加載到緩存中;有一些表記錄不須要加載到緩存當中(好比日誌表);同時也有一些表的記錄會過時,只須要加載指定條件的記錄到緩存使用就能夠了,對於後者咱們就可使用GetExpression()函數來進行控制,只要重寫這個函數,系統在運行GetList()函數時就會自動加載咱們自定義的條件,從數據庫中篩選出咱們想要的記錄到緩存中。函數
CommonBll.cs是邏輯層的工具類,主要提供給模板生成類調用。具體使用方法請看註釋和相關例子。工具
二、邏輯層T4模板文件CreateBll.tt(文件在文章後面下載源碼裏)學習
模板運行後將會生成下圖中的這些函數
其中IIS緩存又包含下圖中這些經常使用函數
原來是想使用Redis來處理緩存的,後來考慮到對於中小型項目來講,不少都沒有獨立的空間,使用的是虛擬機,用Redis也就不是很合適了,因此換成IIS緩存來處理
IIS緩存也由以前的表級別緩存修改成記錄級別了,就是說你對某一條記錄進行添加、刪除、修改、更新等操做時,不用清空整個表緩存,直接對緩存中的記錄進行操做,不過這個功能剛剛改成記錄級別,得Web層代碼開始寫後才能測試看看效果怎麼樣
三、主要模板函數功能說明
1)模板函數調用使用單例模式
對於中小型項目來講,訪問量併發量並非很大,單例模式已經夠用了,若是對某一個表併發量過大時,怕出現問題,也能夠直接new出這個類,不使用單例調用就能夠了
好比:
單例模式調用
InformationBll.GetInstence().GetList();
非單例模式調用
var information = new InformationBll();
information.GetList();
2)表緩存函數
1 private const string const_CacheKey = "Cache_Information"; 2 private const string const_CacheKey_Date = "Cache_Information_Date"; 3
4 #region 清空緩存
5 /// <summary>清空緩存</summary>
6 private void DelAllCache() 7 { 8 //清除模板緩存
9 CacheHelper.RemoveOneCache(const_CacheKey); 10 CacheHelper.RemoveOneCache(const_CacheKey_Date); 11
12 //清除前臺緩存
13 CommonBll.RemoveCache(const_CacheKey); 14 //運行自定義緩存清理程序
15 DelCache(); 16 } 17 #endregion
18
19 #region IIS緩存函數
20
21 #region 從IIS緩存中獲取Information表記錄
22 /// <summary>
23 /// 從IIS緩存中獲取Information表記錄 24 /// </summary>
25 /// <param name="isCache">是否從緩存中讀取</param>
26 public IList<DataAccess.Model.Information> GetList(bool isCache = true) 27 { 28 try
29 { 30 //判斷是否使用緩存
31 if (CommonBll.IsUseCache() && isCache){ 32 //檢查指定緩存是否過時——緩存當天有效,次日自動清空
33 if (CommonBll.CheckCacheIsExpired(const_CacheKey_Date)){ 34 //刪除緩存
35 DelAllCache(); 36 } 37
38 //從緩存中獲取DataTable
39 var obj = CacheHelper.GetCache(const_CacheKey); 40 //若是緩存爲null,則查詢數據庫
41 if (obj == null) 42 { 43 var list = GetList(false); 44
45 //將查詢出來的數據存儲到緩存中
46 CacheHelper.SetCache(const_CacheKey, list); 47 //存儲當前時間
48 CacheHelper.SetCache(const_CacheKey_Date, DateTime.Now); 49
50 return list; 51 } 52 //緩存中存在數據,則直接返回
53 else
54 { 55 return (IList<DataAccess.Model.Information>)obj; 56 } 57 } 58 else
59 { 60 //定義臨時實體集
61 IList<DataAccess.Model.Information> list = null; 62
63 //獲取全表緩存加載條件表達式
64 var exp = GetExpression<Information>(); 65 //若是條件爲空,則查詢全表全部記錄
66 if (exp == null) 67 { 68 //從數據庫中獲取全部記錄
69 var all = Information.All(); 70 list = all == null ? null : Transform(all.ToList()); 71 } 72 else
73 { 74 //從數據庫中查詢出指定條件的記錄,並轉換爲指定實體集
75 var all = Information.Find(exp); 76 list = all == null ? null : Transform(all); 77 } 78
79 return list; 80 } 81 } 82 catch (Exception e) 83 { 84 //記錄日誌
85 CommonBll.WriteLog("從IIS緩存中獲取Information表記錄時出現異常", e); 86 } 87
88 return null; 89 } 90 #endregion
只要調用了GetList()函數,系統就會將全表記錄(重寫GetExpression()函數的,只加載符合條件的記錄)以IList<T>存儲方式緩存到IIS緩存中,供其餘相關函數使用,緩存當天有效,次日訪問時會自動清空從新加載
通常來講,前端與後端在一個項目時,後端操做緩存會直接影響前端的數據。若是先後端分開,作爲兩個項目來開發時,進行增、刪、改操做時就必須調用DelAllCache()函數來清除先後端的緩存,以便兩個站點的緩存能及時同步(目前對於使用IIS緩存跨站點記錄級別緩存同步尚未一個比較好的處理方法,只能直接清空整表緩存來實現),調用DelAllCache()函數時,會執行CommonBll.RemoveCache(const_CacheKey)函數,該函數會經過一個前端訪問接口,發送通過加密處理後的數據,前端接口接收到請求後再清除對應緩存來實現先後端IIS緩存同步功能。
3)實體轉換函數
1 #region 實體轉換
2 /// <summary>
3 /// 將Information記錄實體(SubSonic實體)轉換爲普通的實體(DataAccess.Model.Information) 4 /// </summary>
5 /// <param name="model">SubSonic插件生成的實體</param>
6 /// <returns>DataAccess.Model.Information</returns>
7 public DataAccess.Model.Information Transform(Information model) 8 { 9 if (model == null) 10 return null; 11
12 return new DataAccess.Model.Information 13 { 14 Id = model.Id, 15 InformationClass_Root_Id = model.InformationClass_Root_Id, 16 InformationClass_Root_Name = model.InformationClass_Root_Name, 17 InformationClass_Id = model.InformationClass_Id, 18 InformationClass_Name = model.InformationClass_Name, 19 Title = model.Title, 20 RedirectUrl = model.RedirectUrl, 21 Content = model.Content, 22 Upload = model.Upload, 23 FrontCoverImg = model.FrontCoverImg, 24 Note = model.Note, 25 NewsTime = model.NewsTime, 26 Keywords = model.Keywords, 27 SeoTitle = model.SeoTitle, 28 SeoKey = model.SeoKey, 29 SeoDesc = model.SeoDesc, 30 Author = model.Author, 31 FromName = model.FromName, 32 Sort = model.Sort, 33 IsDisplay = model.IsDisplay, 34 IsHot = model.IsHot, 35 IsTop = model.IsTop, 36 IsPage = model.IsPage, 37 IsDel = model.IsDel, 38 CommentCount = model.CommentCount, 39 ViewCount = model.ViewCount, 40 AddYear = model.AddYear, 41 AddMonth = model.AddMonth, 42 AddDay = model.AddDay, 43 AddDate = model.AddDate, 44 Manager_Id = model.Manager_Id, 45 Manager_CName = model.Manager_CName, 46 UpdateDate = model.UpdateDate, 47 }; 48 } 49
50 /// <summary>
51 /// 將Information記錄實體集(SubSonic實體)轉換爲普通的實體集(DataAccess.Model.Information) 52 /// </summary>
53 /// <param name="sourceList">SubSonic插件生成的實體集</param>
54 public IList<DataAccess.Model.Information> Transform(IList<Information> sourceList) 55 { 56 //建立List容器
57 var list = new List<DataAccess.Model.Information>(); 58 //將SubSonic插件生成的實體集轉換後存儲到剛建立的List容器中
59 sourceList.ToList().ForEach(r => list.Add(Transform(r))); 60 return list; 61 } 62
63 /// <summary>
64 /// 將Information記錄實體由普通的實體(DataAccess.Model.Information)轉換爲SubSonic插件生成的實體 65 /// </summary>
66 /// <param name="model">普通的實體(DataAccess.Model.Information)</param>
67 /// <returns>Information</returns>
68 public Information Transform(DataAccess.Model.Information model) 69 { 70 if (model == null) 71 return null; 72
73 return new Information 74 { 75 Id = model.Id, 76 InformationClass_Root_Id = model.InformationClass_Root_Id, 77 InformationClass_Root_Name = model.InformationClass_Root_Name, 78 InformationClass_Id = model.InformationClass_Id, 79 InformationClass_Name = model.InformationClass_Name, 80 Title = model.Title, 81 RedirectUrl = model.RedirectUrl, 82 Content = model.Content, 83 Upload = model.Upload, 84 FrontCoverImg = model.FrontCoverImg, 85 Note = model.Note, 86 NewsTime = model.NewsTime, 87 Keywords = model.Keywords, 88 SeoTitle = model.SeoTitle, 89 SeoKey = model.SeoKey, 90 SeoDesc = model.SeoDesc, 91 Author = model.Author, 92 FromName = model.FromName, 93 Sort = model.Sort, 94 IsDisplay = model.IsDisplay, 95 IsHot = model.IsHot, 96 IsTop = model.IsTop, 97 IsPage = model.IsPage, 98 IsDel = model.IsDel, 99 CommentCount = model.CommentCount, 100 ViewCount = model.ViewCount, 101 AddYear = model.AddYear, 102 AddMonth = model.AddMonth, 103 AddDay = model.AddDay, 104 AddDate = model.AddDate, 105 Manager_Id = model.Manager_Id, 106 Manager_CName = model.Manager_CName, 107 UpdateDate = model.UpdateDate, 108 }; 109 } 110
111 /// <summary>
112 /// 將Information記錄實體由普通實體集(DataAccess.Model.Information)轉換爲SubSonic插件生成的實體集 113 /// </summary>
114 /// <param name="sourceList">普通實體集(DataAccess.Model.Information)</param>
115 public IList<Information> Transform(IList<DataAccess.Model.Information> sourceList) 116 { 117 //建立List容器
118 var list = new List<Information>(); 119 //將普通實體集轉換後存儲到剛建立的List容器中
120 sourceList.ToList().ForEach(r => list.Add(Transform(r))); 121 return list; 122 } 123 #endregion
因爲SubSonic3.0插件生成的Model附加了不少功能,在對實體進行賦值操做時使用了Linq運算,判斷該字段是否有進行賦值操做,沒有的話在最終生成SQL時,會自動過濾掉該字段。這個功能很是不錯,可是將實體存儲到緩存中或進行Json轉換等操做時,因爲這個運算致使程序進入了死循環,沒法運行,因此必須將它進行一次轉換,轉化爲最多見的普通實體,具體你們能夠查看Solution.DataAccess層SubSonic文件夾下ActiveRecord.tt模板生成的類與CreateModel.tt模板生成的類文件比較一下。實體轉換函數就是將這兩種不一樣的實體進行相互轉換的函數
SubSonic3.0插件ActiveRecord.tt生成的Model
自定義CreateModel.tt模板生成的實體
4)獲取DataTable與綁定Grid表格
1 #region 獲取Information表記錄
2 /// <summary>
3 /// 獲取Information表記錄 4 /// </summary>
5 /// <param name="norepeat">是否使用去重複</param>
6 /// <param name="top">獲取指定數量記錄</param>
7 /// <param name="columns">獲取指定的列記錄</param>
8 /// <param name="pageIndex">當前分頁頁面索引</param>
9 /// <param name="pageSize">每一個頁面記錄數量</param>
10 /// <param name="wheres">查詢條件</param>
11 /// <param name="sorts">排序方式</param>
12 /// <returns>返回DataTable</returns>
13 public DataTable GetDataTable(bool norepeat = false, int top = 0, List<string> columns = null, int pageIndex = 0, int pageSize = 0, List<ConditionFun.SqlqueryCondition> wheres = null, List<string> sorts = null) { 14 try
15 { 16 //分頁查詢
17 var select = new SelectHelper(); 18 return select.SelectDataTable<Information>(norepeat, top, columns, pageIndex, pageSize, wheres, sorts); 19 } 20 catch (Exception e) 21 { 22 //記錄日誌
23 CommonBll.WriteLog("獲取Information表記錄時出現異常", e); 24
25 return null; 26 } 27 } 28 #endregion
29
30 #region 綁定Grid表格
31 /// <summary>
32 /// 綁定Grid表格,並實現分頁 33 /// </summary>
34 /// <param name="grid">表格控件</param>
35 /// <param name="pageIndex">第幾頁</param>
36 /// <param name="pageSize">每頁顯示記錄數量</param>
37 /// <param name="wheres">查詢條件</param>
38 /// <param name="sorts">排序</param>
39 public void BindGrid(FineUI.Grid grid, int pageIndex = 0, int pageSize = 0, List<ConditionFun.SqlqueryCondition> wheres = null, List<string> sorts = null) { 40 //用於統計執行時長(耗時)
41 var swatch = new Stopwatch(); 42 swatch.Start(); 43
44 try { 45 // 1.設置總項數
46 grid.RecordCount = GetRecordCount(wheres); 47 // 若是不存在記錄,則清空Grid表格
48 if (grid.RecordCount == 0) { 49 grid.Rows.Clear(); 50 return; 51 } 52
53 // 2.查詢並綁定到Grid
54 grid.DataSource = GetDataTable(false, 0, null, pageIndex, pageSize, wheres, sorts); 55 grid.DataBind(); 56
57 } 58 catch (Exception e) { 59 // 記錄日誌
60 CommonBll.WriteLog("獲取用戶操做日誌表記錄時出現異常", e); 61
62 } 63
64 // 統計結束
65 swatch.Stop(); 66 // 計算查詢數據庫使用時間,並存儲到Session裏,以便UI顯示
67 HttpContext.Current.Session["SpendingTime"] = (swatch.ElapsedMilliseconds / 1000.00).ToString(); 68 } 69 #endregion
70
71 #region 綁定Grid表格
72 /// <summary>
73 /// 綁定Grid表格,使用內存分頁,顯示有層次感 74 /// </summary>
75 /// <param name="grid">表格控件</param>
76 /// <param name="parentValue">父Id值</param>
77 /// <param name="wheres">查詢條件</param>
78 /// <param name="sorts">排序</param>
79 /// <param name="parentId">父Id</param>
80 public void BindGrid(FineUI.Grid grid, int parentValue, List<ConditionFun.SqlqueryCondition> wheres = null, List<string> sorts = null, string parentId = "ParentId") { 81 //用於統計執行時長(耗時)
82 var swatch = new Stopwatch(); 83 swatch.Start(); 84
85 try
86 { 87 // 查詢數據庫
88 var dt = GetDataTable(false, 0, null, 0, 0, wheres, sorts); 89
90 // 對查詢出來的記錄進行層次處理
91 grid.DataSource = DataTableHelper.DataTableTidyUp(dt, "Id", parentId, parentValue); 92 // 查詢並綁定到Grid
93 grid.DataBind(); 94
95 } 96 catch (Exception e) { 97 // 記錄日誌
98 CommonBll.WriteLog("綁定表格時出現異常", e); 99
100 } 101
102 //統計結束
103 swatch.Stop(); 104 //計算查詢數據庫使用時間,並存儲到Session裏,以便UI顯示
105 HttpContext.Current.Session["SpendingTime"] = (swatch.ElapsedMilliseconds / 1000.00).ToString(); 106 } 107
108 /// <summary>
109 /// 綁定Grid表格,使用內存分頁,顯示有層次感 110 /// </summary>
111 /// <param name="grid">表格控件</param>
112 /// <param name="parentValue">父Id值</param>
113 /// <param name="sorts">排序</param>
114 /// <param name="parentId">父Id</param>
115 public void BindGrid(FineUI.Grid grid, int parentValue, List<string> sorts = null, string parentId = "ParentId") { 116 BindGrid(grid, parentValue, null, sorts, parentId); 117 } 118 #endregion
主要是用於經過條件查詢,獲取指定表記錄,而後與後端的FineUI.Grid綁定,具體使用例子會在Web層的相關章節中說明
你們在看源碼時會發現CommonBll.WriteLog("獲取Information表記錄時出現異常", e);這樣的代碼,其實在整個框架中這種代碼大量存在,它會將出現的異常或操做步驟,忠實的記錄指定目錄的文本文件中,方便咱們查看分析。特別是項目上線後,在生產環境中咱們不少時候是不能直接對生產環境進行測試的,而用戶是作了什麼操做纔出現這種異常的沒有日誌記錄就很難進行排查,因此咱們在編寫自定義邏輯層函數時,也隨手將這樣的代碼寫上,以方便咱們之後分析問題。
而HttpContext.Current.Session["SpendingTime"] = (swatch.ElapsedMilliseconds / 1000.00).ToString();這種代碼記錄的是執行查詢綁定FineUI.Grid花費的時間,並會在Web層相關列表頁面顯示出來,方便咱們瞭解頁面的執行效率。
模版會生成兩種類型的Grid表格綁定函數,一個是正常的列表綁定,一個是用於多級分類時有層次感顯示的列表綁定。正常列表是直接從數據庫中查詢出來,而有層次感的列表,使用的是內存分頁,而不是整表緩存。它是將按條件查詢出來的記錄緩存到內存中,在點擊翻頁時,是在內存中獲取分頁而後顯示。
有層次感列表例子:
5)添加與編輯記錄函數
1 #region 添加與編輯Information表記錄
2 /// <summary>
3 /// 添加與編輯Information記錄 4 /// </summary>
5 /// <param name="page">當前頁面指針</param>
6 /// <param name="model">Information表實體</param>
7 /// <param name="isCache">是否更新緩存</param>
8 public void Save(Page page, Information model, bool isCache = true) 9 { 10 try { 11 //保存
12 model.Save(); 13
14 //判斷是否啓用緩存
15 if (CommonBll.IsUseCache() && isCache) 16 { 17 SetModelForCache(model); 18 } 19
20 //添加用戶訪問記錄
21 UseLogBll.GetInstence().Save(page, "{0}" + (model.Id == 0 ? "添加" : "編輯") + "Information記錄成功,ID爲【" + model.Id + "】"); 22 } 23 catch (Exception e) { 24 var result = "執行InformationBll.Save()函數出錯!"; 25
26 //出現異常,保存出錯日誌信息
27 CommonBll.WriteLog(result, e); 28 } 29 } 30 #endregion
執行更新操做後,會檢查是否啓用了緩存功能,啓用了由會同步更新緩存記錄。而後執行用戶操做日誌記錄功能,將用戶執行了什麼操做更新到對應的數據庫中。其餘刪除與更新操做都會作一樣的記錄。(有了詳細的操做日誌記錄,萬一後端系統管理人員操做出了什麼問題,要落實操做責任也就很容易查找出來)
6)保存排序與自動排序函數
1 #region 保存列表排序
2 /// <summary>
3 /// 保存列表排序,若是使用了緩存,保存成功後會清空本表的全部緩存記錄,而後從新加載進緩存 4 /// </summary>
5 /// <param name="page">當前頁面指針</param>
6 /// <param name="grid1">頁面表格</param>
7 /// <param name="tbxSortId">表格中綁定排序的表單名</param>
8 /// <param name="sortIdName">排序字段名</param>
9 /// <returns>更新成功返回true,失敗返回false</returns>
10 public bool UpdateSort(Page page, FineUI.Grid grid1, string tbxSortId, string sortIdName = "SortId") 11 { 12 //更新排序
13 if (CommonBll.UpdateSort(page, grid1, tbxSortId, "Information", sortIdName, "Id")) 14 { 15 //判斷是否啓用緩存
16 if (CommonBll.IsUseCache()) 17 { 18 //刪除全部緩存
19 DelAllCache(); 20 //從新載入緩存
21 GetList(); 22 } 23
24 //添加用戶操做記錄
25 UseLogBll.GetInstence().Save(page, "{0}更新了Information表排序!"); 26
27 return true; 28 } 29
30 return false; 31 } 32 #endregion
33
34 #region 自動排序
35 /// <summary>自動排序,若是使用了緩存,保存成功後會清空本表的全部緩存記錄,而後從新加載進緩存</summary>
36 /// <param name="page">當前頁面指針</param>
37 /// <param name="strWhere">附加Where : " sid=1 "</param>
38 /// <param name="isExistsMoreLv">是否存在多級分類,一級時,請使用false,多級使用true,(一級不包括ParentID字段)</param>
39 /// <param name="pid">父級分類的ParentID</param>
40 /// <param name="fieldName">字段名:"SortId"</param>
41 /// <param name="fieldParentId">字段名:"ParentId"</param>
42 /// <returns>更新成功返回true,失敗返回false</returns>
43 public bool UpdateAutoSort(Page page, string strWhere = "", bool isExistsMoreLv = false, int pid = 0, string fieldName = "SortId", string fieldParentId = "ParentId") 44 { 45 //更新排序
46 if (CommonBll.AutoSort("Id", "Information", strWhere, isExistsMoreLv, pid, fieldName, fieldParentId)) 47 { 48 //判斷是否啓用緩存
49 if (CommonBll.IsUseCache()) 50 { 51 //刪除全部緩存
52 DelAllCache(); 53 //從新載入緩存
54 GetList(); 55 } 56
57 //添加用戶操做記錄
58 UseLogBll.GetInstence().Save(page, "{0}對Information表進行了自動排序操做!"); 59
60 return true; 61 } 62
63 return false; 64 } 65 #endregion
保存排序函數是將列表中直接填寫的排序直接保存到數據庫中
自動排序函數執行後,會將當前頁面所綁定表格的全部記錄分級別所有從小到大從新進行排序,好比二級分類原排序值爲一、二、五、十、十一、12,執行後會變成一、二、三、四、五、6。
同時會清除緩存,而且添加用戶操做記錄。
Web層會將這兩個函數進行封裝,處理後無需再編寫代碼,只要添加了對應按鍵就會自支繼承相應功能,減小Web層的重複代碼編寫
因爲使用了新的工具類庫,模板也作了一些修改,代碼實現就花了好幾天才完成,而直接完成代碼後,文章都不知道怎麼寫才合適了,今天完成後反覆看了幾遍,都以爲寫得差強人意,呵呵......
除了上面介紹的函數外,還有其餘類與模板函數你們本身看吧,有什麼問題再發上來你們一塊兒討論
四、小結
寫到這裏,其實框架的底層結構算是基本完成的,T4模板與SubSonic3.0的結合,產生一個輕量級的開發框架,不管是開發Winform、Web服務仍是其餘軟件,在這個組合下均可以令咱們輕鬆應對,去除了大量的重複編碼時間,輕輕鬆鬆一鍵生成咱們須要的大部分代碼。而模板設計合理的話,應用一些新技術或替換某些功能(好比IIS模板換成Redis模板),Web層基本上不用作什麼修改就能夠直接使用了。對於數據庫添加新表新字段,修改或刪除字段操做,也變得很輕鬆。因爲整個設計不存在硬編碼,就算有些地方要修改,運行一下編譯就能立刻知道須要修改那個位置。
點擊下載:
版權聲明:
本文由AllEmpty原創併發佈於博客園,歡迎轉載,未經本人贊成必須保留此段聲明,且在文章頁面明顯位置給出原文連接,不然保留追究法律責任的權利。若有問題,能夠經過1654937@qq.com 聯繫我,很是感謝。
發表本編內容,只要主爲了和你們共同窗習共同進步,有興趣的朋友能夠加加Q羣:327360708 ,你們一塊兒探討。
更多內容,敬請觀注博客:http://www.cnblogs.com/EmptyFS/