很多网友建议在YbRapidSolution for MVC框架的基础上实现CMS功能,以方便进行内容的管理,加快前端页面的开发速度。因此花了一段时间,实现了一套CMS内容发布系统并已集成至YbRapidSolution for MVC框架中。
本CMS当前实现了CMS参数设置、栏目管理、文章管理、文档管理、评论管理、问卷调查等功能。首先看看本CMS使用的主要技术及其整体架构图:
上图中的架构可以说和YbRapidSolution for MVC的架构基本是一致的,如下将对主要的技术要点进行总结和介绍:
1、CMS参数设置
CMS参数设置主要对一些全局信息的内容和设置进行管理,如网站标题、Logo、版权信息等。其底层使用了应用程序设置公共组件。仅需简单实现自Yb.Data.Provider.BaseSettings类即可,同时可随意声明自身需要的属性,非常方便,代码如下:
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 }
2、Entity Framework中的多主键的配置
文章、文档、评论等的管理,通常需要支持草稿和发布两种状态,而且两种状态在管理时互不影响。例如发布了一篇文章后,还应可继续编辑该文章并保持其为草稿状态,在未重新发布之前,不会覆盖已发布的内容;而一旦草稿的内容发布后将自动更新发布的内容,相反也可提供草稿的撤销功能(即用已发布的内容来覆盖现有草稿的内容)。因此对文章、文档和评论的管理使用了双主键,一个主键代表标识(字段名为ID),一个主键代表是否是草稿状态(字段名为Draft),同一ID标识的内容也就有了草稿和发布两个版本的内容。在国内,内容编辑人员和签发(即发布)人员通常不是同一个人,因此使用本设计也能满足国内环境下较为特殊的操作权限需求。
在EF中,一个实体设置多主键需要使用类似下面的语句:
this.HasKey(c => new { c.ID, c.Draft });
3、Entity Framework中的一对多和多对多的映射配置
国内的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);
4、文档的管理
文档管理主要是对上传的内容(文件、图片、音频、视频等)进行管理,本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/进一步了解。