不少網友建議在YbRapidSolution for MVC框架的基礎上實現CMS功能,以方便進行內容的管理,加快前端頁面的開發速度。所以花了一段時間,實現了一套CMS內容發佈系統並已集成至YbRapidSolution for MVC框架中。css
本CMS當前實現了CMS參數設置、欄目管理、文章管理、文檔管理、評論管理、問卷調查等功能。首先看看本CMS使用的主要技術及其總體架構圖:html
上圖中的架構能夠說和YbRapidSolution for MVC的架構基本是一致的,以下將對主要的技術要點進行總結和介紹:前端
一、CMS參數設置數據庫
CMS參數設置主要對一些全局信息的內容和設置進行管理,如網站標題、Logo、版權信息等。其底層使用了應用程序設置公共組件。僅需簡單實現自Yb.Data.Provider.BaseSettings類便可,同時可隨意聲明自身須要的屬性,很是方便,代碼以下:api
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Linq; 5 using System.Runtime.Serialization; 6 using System.Text; 7 namespace YbRapidSolution.Services.Cms 8 { 9 [Serializable] 10 [DataContract] 11 public class CmsSetting : Yb.Data.Provider.BaseSettings 12 { 13 /// <summary> 14 /// 是否多站點共享 15 /// </summary> 16 public override bool AsShared 17 { 18 get { return false; } 19 } 20 21 #region 站點參數 22 23 /// <summary> 24 /// 站點名稱 25 /// </summary> 26 [DisplayName("站點名稱")] 27 [Description("本站點的名稱")] 28 [DefaultValue("Yellbuy")] 29 [DataMember] 30 public string SiteName { get; set; } 31 /// <summary> 32 /// 站點域名 33 /// </summary> 34 [DisplayName("站點域名")] 35 [Description("本站點的域名")] 36 [DefaultValue("http://Yellbuy.com")] 37 [DataMember] 38 public string SiteDomain { get; set; } 39 /// <summary> 40 /// 站點標題 41 /// </summary> 42 [DisplayName("站點標題")] 43 [Description("本站點的標題")] 44 [DefaultValue("Yellbuy")] 45 [DataMember] 46 public string SiteTitle { get; set; } 47 /// <summary> 48 /// 站點Logo地址 49 /// </summary> 50 [DisplayName("站點Logo地址")] 51 [Description("本站點的Logo地址")] 52 [DefaultValue("")] 53 [DataMember] 54 public string SiteLogoUrl { get; set; } 55 /// <summary> 56 /// 站點Banner地址 57 /// </summary> 58 [DisplayName("站點Banner地址")] 59 [Description("本站點的Banner地址")] 60 [DefaultValue("")] 61 [DataMember] 62 public string SiteBannerUrl { get; set; } 63 /// <summary> 64 /// 站點版權信息 65 /// </summary> 66 [DisplayName("站點版權信息")] 67 [Description("本站點的版權信息")] 68 [DefaultValue("© 2010-2015 YELLBY.版權全部")] 69 [DataMember] 70 public string SiteCopyRight { get; set; } 71 /// <summary> 72 /// 站點關鍵字 73 /// </summary> 74 [DisplayName("站點關鍵字")] 75 [Description("本站點的關鍵字")] 76 [DefaultValue("")] 77 [DataMember] 78 public string SiteKeyword { get; set; } 79 /// <summary> 80 /// 站點描述 81 /// </summary> 82 [DisplayName("站點描述")] 83 [Description("本站點的描述信息")] 84 [DefaultValue("")] 85 [DataMember] 86 public string SiteDescription { get; set; } 87 /// <summary> 88 /// 站點首頁路徑 89 /// </summary> 90 [DisplayName("站點首頁路徑")] 91 [Description("本站點的首頁路徑")] 92 [DefaultValue("")] 93 [DataMember] 94 public string SiteHomePath { get; set; } 95 96 #endregion 97 98 #region 郵件參數 99 100 /// <summary> 101 /// Email地址 102 /// </summary> 103 [DisplayName("Email地址")] 104 [Description("Email地址")] 105 [DefaultValue("19892257@qq.com")] 106 [DataMember] 107 public virtual string EmailAddress { get; set; } 108 109 /// <summary> 110 /// Email顯示名 111 /// </summary> 112 [DisplayName("Email顯示名")] 113 [Description("Email顯示名")] 114 [DefaultValue("Yellbuy")] 115 [DataMember] 116 public string EmailDisplayName { get; set; } 117 118 /// <summary> 119 /// Email主機名 120 /// </summary> 121 [DisplayName("Email主機名")] 122 [Description("Email主機名")] 123 [DefaultValue("")] 124 [DataMember] 125 public string EmailHost { get; set; } 126 127 /// <summary> 128 /// Email端口號 129 /// </summary> 130 [DisplayName("Email端口號")] 131 [Description("Email端口號")] 132 [DefaultValue(25)] 133 [DataMember] 134 public int EmailPort { get; set; } 135 136 /// <summary> 137 /// Email用戶名 138 /// </summary> 139 [DisplayName("Email用戶名")] 140 [Description("Email用戶名")] 141 [DefaultValue("")] 142 [DataMember] 143 public string EmailUsername { get; set; } 144 145 /// <summary> 146 /// Email密碼 147 /// </summary> 148 [DisplayName("Email密碼")] 149 [Description("Email密碼")] 150 [DefaultValue("")] 151 [DataMember] 152 public string EmailPassword { get; set; } 153 154 /// <summary> 155 /// Email是否使用SSL 156 /// </summary> 157 [DisplayName("Email是否使用SSL")] 158 [Description("Email是否使用SSL")] 159 [DefaultValue(false)] 160 [DataMember] 161 public bool EnableSsl { get; set; } 162 163 /// <summary> 164 /// Email是否使用默認證書 165 /// </summary> 166 [DisplayName("Email是否使用默認證書")] 167 [Description("Email是否使用默認證書")] 168 [DefaultValue(false)] 169 [DataMember] 170 public bool EmailUseDefaultCredentials { get; set; } 171 172 /// <summary> 173 /// Gets a friendly email account name 174 /// </summary> 175 public string GetFriendlyName() 176 { 177 if (!String.IsNullOrWhiteSpace(this.EmailDisplayName)) 178 return this.EmailAddress + " (" + this.EmailDisplayName + ")"; 179 return this.EmailAddress; 180 } 181 182 #endregion 183 184 #region 頁面參數 185 186 /// <summary> 187 /// 頁面緩存類型 188 /// </summary> 189 [DisplayName("頁面緩存類型")] 190 [Description("0:不緩存,1:絕對時間失效,2:相對時間失效")] 191 [DefaultValue(0)] 192 [DataMember] 193 public int PageCacheType { get; set; } 194 /// <summary> 195 /// 頁面緩存時間 196 /// </summary> 197 [DisplayName("頁面緩存時間")] 198 [Description("單位:秒(S)")] 199 [DefaultValue(10)] 200 [DataMember] 201 public int PageCacheInterval { get; set; } 202 /// <summary> 203 /// 內容過濾字符串 204 /// </summary> 205 [DisplayName("內容過濾字符串")] 206 [Description("多個內容使用英文半角「,」符合進行分割")] 207 [DefaultValue("")] 208 [DataMember] 209 public string PageFilter { get; set; } 210 /// <summary> 211 /// 容許上傳的圖片格式 212 /// </summary> 213 [DisplayName("容許上傳的圖片格式")] 214 [Description("多個內容使用英文半角「,」符合進行分割")] 215 [DefaultValue(".gif,.jpg,.jpeg,.bmp,.png")] 216 [DataMember] 217 public string PageUploadAllowImageFormat { get; set; } 218 219 public string[] GetUploadAllowImageFormat() 220 { 221 if (string.IsNullOrWhiteSpace(PageUploadAllowImageFormat)) return new string[0]; 222 return PageUploadAllowImageFormat.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); 223 } 224 /// <summary> 225 /// 容許上傳的圖片大小 226 /// </summary> 227 [DisplayName("容許上傳的圖片大小")] 228 [Description("單位:KB,0表示不限制大小")] 229 [DefaultValue(4096)] 230 [DataMember] 231 public int PageUploadAllowImageSize { get; set; } 232 /// <summary> 233 /// 容許上傳的音頻格式 234 /// </summary> 235 [DisplayName("容許上傳的音頻格式")] 236 [Description("多個內容使用英文半角「,」符合進行分割")] 237 [DefaultValue(".mid,.mp3,.wma")] 238 [DataMember] 239 public string PageUploadAllowAudioFormat { get; set; } 240 public string[] GetUploadAllowAudioFormat() 241 { 242 if (string.IsNullOrWhiteSpace(PageUploadAllowAudioFormat)) return new string[0]; 243 return PageUploadAllowAudioFormat.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 244 } 245 /// <summary> 246 /// 容許上傳的音頻大小 247 /// </summary> 248 [DisplayName("容許上傳的音頻大小")] 249 [Description("單位:KB,0表示不限制大小")] 250 [DefaultValue(4096)] 251 [DataMember] 252 public int PageUploadAllowAudioSize { get; set; } 253 /// <summary> 254 /// 容許上傳的視頻格式 255 /// </summary> 256 [DisplayName("容許上傳的視頻格式")] 257 [Description("多個內容使用英文半角「,」符合進行分割")] 258 [DefaultValue(".wmv,.asf,.avi,.mpg,.ram,.rm,.swf")] 259 [DataMember] 260 public string PageUploadAllowVideoFormat { get; set; } 261 public string[] GetUploadAllowVideoFormat() 262 { 263 if (string.IsNullOrWhiteSpace(PageUploadAllowVideoFormat)) return new string[0]; 264 return PageUploadAllowVideoFormat.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 265 } 266 /// <summary> 267 /// 容許上傳的視頻大小 268 /// </summary> 269 [DisplayName("容許上傳的視頻大小")] 270 [Description("單位:KB,0表示不限制大小")] 271 [DefaultValue(4096)] 272 [DataMember] 273 public int PageUploadAllowVideoSize { get; set; } 274 /// <summary> 275 /// 容許上傳的文檔文件格式 276 /// </summary> 277 [DisplayName("容許上傳的文檔文件格式")] 278 [Description("多個內容使用英文半角「,」符合進行分割")] 279 [DefaultValue(".rar,.zip,doc,docx,xls,xlsx,pdf")] 280 [DataMember] 281 public string PageUploadAllowDocumentFormat { get; set; } 282 public string[] GetUploadAllowDocumentFormat() 283 { 284 if (string.IsNullOrWhiteSpace(PageUploadAllowDocumentFormat)) return new string[0]; 285 return PageUploadAllowDocumentFormat.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 286 } 287 /// <summary> 288 /// 容許上傳的文檔文件大小 289 /// </summary> 290 [DisplayName("容許上傳的文檔文件大小")] 291 [Description("單位:KB,0表示不限制大小")] 292 [DefaultValue(4096)] 293 [DataMember] 294 public int PageUploadAllowDocumentSize { get; set; } 295 /// <summary> 296 /// 容許上傳的其餘文件格式 297 /// </summary> 298 [DisplayName("容許上傳的其餘文件格式")] 299 [Description("多個內容使用英文半角「,」符合進行分割")] 300 [DefaultValue(".html,.htm,.css,cshtml,aspx")] 301 [DataMember] 302 public string PageUploadAllowFileFormat { get; set; } 303 public string[] GetUploadAllowFileFormat() 304 { 305 if (string.IsNullOrWhiteSpace(PageUploadAllowFileFormat)) return new string[0]; 306 return PageUploadAllowFileFormat.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 307 } 308 /// <summary> 309 /// 容許上傳的其餘文件大小 310 /// </summary> 311 [DisplayName("容許上傳的文件大小")] 312 [Description("單位:KB,0表示不限制大小")] 313 [DefaultValue(4096)] 314 [DataMember] 315 public int PageUploadAllowFileSize { get; set; } 316 #endregion 317 318 #region 用戶設置 319 320 /// <summary> 321 /// 後臺登陸使用驗證碼 322 /// </summary> 323 [DisplayName("後臺登陸使用驗證碼")] 324 [Description("後臺登陸使用驗證碼")] 325 [DefaultValue(false)] 326 [DataMember] 327 public bool AdminUseAuthCode { get; set; } 328 329 /// <summary> 330 /// 容許用戶註冊 331 /// </summary> 332 [DisplayName("容許用戶註冊")] 333 [Description("是否容許用戶註冊")] 334 [DefaultValue(false)] 335 [DataMember] 336 public bool UserAllowRegister { get; set; } 337 /// <summary> 338 /// 用戶註冊是否須要使用Email驗證 339 /// </summary> 340 [DisplayName("用戶註冊是否須要使用Email驗證")] 341 [Description("用戶註冊是否須要使用Email驗證")] 342 [DefaultValue(false)] 343 [DataMember] 344 public bool UserUseEmailValidate { get; set; } 345 /// <summary> 346 /// 用戶註冊時是否使用驗證碼 347 /// </summary> 348 [DisplayName("用戶註冊時是否使用驗證碼")] 349 [Description("用戶註冊時是否使用驗證碼")] 350 [DefaultValue(false)] 351 [DataMember] 352 public bool UserRegisterUseAuthCode { get; set; } 353 /// <summary> 354 /// 用戶登陸時是否使用驗證碼 355 /// </summary> 356 [DisplayName("用戶登陸時是否使用驗證碼")] 357 [Description("用戶登陸時是否使用驗證碼")] 358 [DefaultValue(false)] 359 [DataMember] 360 public bool UserLoginUseAuthCode { get; set; } 361 /// <summary> 362 /// 用戶回覆方式 363 /// </summary> 364 [DisplayName("用戶回覆方式")] 365 [Description("0:不容許回覆,1:容許匿名用戶回覆,2:僅容許登陸用戶回覆")] 366 [DefaultValue(0)] 367 [DataMember] 368 public int UserReplyKind { get; set; } 369 /// <summary> 370 /// 用戶回覆時是否輸入驗證碼 371 /// </summary> 372 [DisplayName("用戶回覆時是否輸入驗證碼")] 373 [Description("用戶回覆時是否輸入驗證碼")] 374 [DefaultValue(false)] 375 [DataMember] 376 public bool UserReplyUseAuthCode { get; set; } 377 /// <summary> 378 /// 用戶回覆時是否加載編輯器 379 /// </summary> 380 [DisplayName("用戶回覆時是否加載編輯器")] 381 [Description("用戶回覆時是否加載編輯器")] 382 [DefaultValue(false)] 383 [DataMember] 384 public bool UserReplyUseEditor { get; set; } 385 /// <summary> 386 /// 用戶回覆內容是否須要審覈才容許發佈 387 /// </summary> 388 [DisplayName("用戶回覆是否須要審覈")] 389 [Description("用戶回覆內容是否須要審覈才容許發佈")] 390 [DefaultValue(false)] 391 [DataMember] 392 public bool UserReplyAudit { get; set; } 393 394 #endregion 395 396 #region 水印/縮放設置 397 398 /// <summary> 399 /// 水印類型 400 /// </summary> 401 [DisplayName("水印類型")] 402 [Description("水印類型,0:文字水印,1:圖片水印")] 403 [DefaultValue(0)] 404 [DataMember] 405 public int WatermarkKind { get; set; } 406 /// <summary> 407 /// 文字水印內容 408 /// </summary> 409 [DisplayName("文字水印內容")] 410 [Description("文字水印內容")] 411 [DefaultValue("YELLBUY")] 412 [DataMember] 413 public string WatermarkText { get; set; } 414 /// <summary> 415 /// 文字水印字體大小 416 /// </summary> 417 [DisplayName("文字水印字體大小")] 418 [Description("單位:pt")] 419 [DefaultValue(11)] 420 [DataMember] 421 public int WatermarkFontSize { get; set; } 422 /// <summary> 423 /// 文字水印字體名稱 424 /// </summary> 425 [DisplayName("文字水印字體名稱")] 426 [Description("")] 427 [DefaultValue("Arial")] 428 [DataMember] 429 public string WatermarkFontFamily { get; set; } 430 /// <summary> 431 /// 文字水印字體顏色 432 /// </summary> 433 [DisplayName("文字水印字體顏色")] 434 [Description("")] 435 [DefaultValue("#Blue")] 436 [DataMember] 437 public string WatermarkFontColor { get; set; } 438 /// <summary> 439 /// 圖片水印的圖片地址 440 /// </summary> 441 [DisplayName("圖片水印的圖片地址")] 442 [Description("")] 443 [DefaultValue("")] 444 [DataMember] 445 public string WatermarkImgUrl { get; set; } 446 /// <summary> 447 /// 圖片水印的高度 448 /// </summary> 449 [DisplayName("圖片水印的高度")] 450 [Description("單位:px")] 451 [DefaultValue(8)] 452 [DataMember] 453 public int WatermarkImgHeight { get; set; } 454 /// <summary> 455 /// 圖片水印的寬度 456 /// </summary> 457 [DisplayName("圖片水印的寬度")] 458 [Description("單位:px")] 459 [DefaultValue(16)] 460 [DataMember] 461 public int WatermarkImgWidth { get; set; } 462 /// <summary> 463 /// 圖片水印的透明度 464 /// </summary> 465 [DisplayName("圖片水印的透明度")] 466 [Description("在0%至100%之間,100%表示不透明,0%表示徹底透明")] 467 [DefaultValue(0.5f)] 468 [DataMember] 469 public float WatermarkImgOpacity { get; set; } 470 /// <summary> 471 /// 圖片水印的位置 472 /// </summary> 473 [DisplayName("圖片水印的位置")] 474 [Description("0:左上角,1:右上角,2:左下角,3:右下角")] 475 [DefaultValue(3)] 476 [DataMember] 477 public int WatermarkImgRegion { get; set; } 478 479 #endregion 480 } 481 }
數據訪問方面,僅需調用SettingApi.LoadSettings便可進行上述參數設置信息的加載,使用SettingApi.SaveSettings方法則進行參數設置信息的保存,最終的參數設置信息將保存至數據庫中。數據訪問的具體代碼以下(注:以下代碼中對CMS參數設置信息進行了內存緩存處理以提升系統性能):緩存
1 public CmsSetting Load() 2 { 3 string key = CACHE_PATTERN_KEY; 4 return _cacheManager.Get(key, () => 5 { 6 var pv = SettingApi.LoadSettings<CmsSetting>(); 7 return pv; 8 }); 9 } 10 11 public void Save(CmsSetting setting) 12 { 13 if (setting == null) 14 throw new ArgumentNullException("setting"); 15 16 SettingApi.SaveSettings(setting); 17 //移除緩存 18 _cacheManager.RemoveByPattern(CACHE_PATTERN_KEY); 19 }
二、Entity Framework中的多主鍵的配置架構
文章、文檔、評論等的管理,一般須要支持草稿和發佈兩種狀態,並且兩種狀態在管理時互不影響。例如發佈了一篇文章後,還應可繼續編輯該文章並保持其爲草稿狀態,在未從新發布以前,不會覆蓋已發佈的內容;而一旦草稿的內容發佈後將自動更新發布的內容,相反也可提供草稿的撤銷功能(即用已發佈的內容來覆蓋現有草稿的內容)。所以對文章、文檔和評論的管理使用了雙主鍵,一個主鍵表明標識(字段名爲ID),一個主鍵表明是不是草稿狀態(字段名爲Draft),同一ID標識的內容也就有了草稿和發佈兩個版本的內容。在國內,內容編輯人員和簽發(即發佈)人員一般不是同一我的,所以使用本設計也能知足國內環境下較爲特殊的操做權限需求。框架
在EF中,一個實體設置多主鍵須要使用相似下面的語句:編輯器
this.HasKey(c => new { c.ID, c.Draft });
三、Entity Framework中的一對多和多對多的映射配置ide
國內的CMS一般都有個欄目的概念,其實質就是網站的組織結構。在本CMS中,文章、頁面、文檔等均能添加至欄目中。以文章爲例,由於一篇文章可對應多個欄目、一個欄目下可能有多個文章,所以是N-N的關係,該N-N關係在Entity Framework中的文章Map配置以下:
1 this.HasMany(p => p.CmsColumn).WithMany().Map(pc => 2 { 3 pc.ToTable("YbCmsRelation"); 4 pc.MapLeftKey(new string[] { "DataId", "Draft" }); 5 pc.MapRightKey("RelatedId"); 6 });
上述代碼中,使用了一個關聯表"YbCmsRelation",該表中的」DataId"和「Draft」對應文章的主鍵字段,「RelatedId"則對應欄目表(CmsColumn)中的主鍵字段,上述配置最大的優勢是不會在文章實體和欄目實體中出現中間關聯的導航屬性,可直接跳過CmsRelation直接訪問欄目,很是的方便。
至於一對多關係則簡單得多,以問卷調查爲例,一項調查有多個可選結果,可在「可選結果」一方的Map配置文件中進行以下示例配置並指明瞭「PollItemId」爲外鍵:
//關聯映射配置 this.HasRequired(t => t.CmsPollItem) .WithMany(t => t.CmsPollAnswer) .HasForeignKey(t => t.PollItemId) .WillCascadeOnDelete(false);
四、文檔的管理
文檔管理主要是對上傳的內容(文件、圖片、音頻、視頻等)進行管理,本CMS中不只須要對全部編輯器上傳的內容進行集中管理,同時也須要支持草稿和發佈兩種狀態、支持樹形結構的管理等,所以本CMS對UEditor和KindEditor等編輯器默認提供的Controller進行了必要的重寫,以讓其適應新的需求;在具體過程當中,還實現了對圖片的優化處理,例如可按圖片請求的尺寸生成對應尺寸的縮略圖並進行緩存處理等。
生成縮略圖的代碼以下:
1 public static Image GetThumbnail(Image img, int size) 2 { 3 // 生成縮略圖 4 var bmp = new Bitmap(size, size); 5 using (var grp = Graphics.FromImage(bmp)) 6 { 7 grp.SmoothingMode = SmoothingMode.HighQuality; 8 grp.CompositingQuality = CompositingQuality.HighQuality; 9 grp.InterpolationMode = InterpolationMode.High; 10 11 // Resize and crop image 12 var dst = new Rectangle(0, 0, bmp.Width, bmp.Height); 13 grp.DrawImage(img, dst, img.Width > img.Height ? (img.Width - img.Height)/2 : 0, 14 img.Height > img.Width ? (img.Height - img.Width)/2 : 0, Math.Min(img.Width, img.Height), 15 Math.Min(img.Height, img.Width), GraphicsUnit.Pixel); 16 grp.Dispose(); 17 } 18 return bmp; 19 }
總結:目前國內.NET平臺的 CMS 大部分均以 ASP.NET WebForm 爲主,其實以CMS的特色來看,使用 ASP.NET MVC 進行開發無疑更加容易上手,開發和維護也更加方便和快捷。本CMS系統將在現有版本的基礎之上提供更多符合國內使用習慣的功能和模塊,具體可訪問http://pjdemo.yellbuy.com/進一步瞭解。