很多网友建议在YbRapidSolution for MVC框架的基础上实现CMS功能,以方便进行内容的管理,加快前端页面的开发速度。因此花了一段时间,实现了一套CMS内容发布系统并已集成至YbRapidSolution for MVC框架中。

本CMS当前实现了CMS参数设置、栏目管理、文章管理、文档管理、评论管理、问卷调查等功能。首先看看本CMS使用的主要技术及其整体架构图:

上图中的架构可以说和YbRapidSolution for MVC的架构基本是一致的,如下将对主要的技术要点进行总结和介绍:

1、CMS参数设置

CMS参数设置主要对一些全局信息的内容和设置进行管理,如网站标题、Logo、版权信息等。其底层使用了应用程序设置公共组件。仅需简单实现自Yb.Data.Provider.BaseSettings类即可,同时可随意声明自身需要的属性,非常方便,代码如下:

 using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
namespace YbRapidSolution.Services.Cms
{
[Serializable]
[DataContract]
public class CmsSetting : Yb.Data.Provider.BaseSettings
{
/// <summary>
/// 是否多站点共享
/// </summary>
public override bool AsShared
{
get { return false; }
} #region 站点参数 /// <summary>
/// 站点名称
/// </summary>
[DisplayName("站点名称")]
[Description("本站点的名称")]
[DefaultValue("Yellbuy")]
[DataMember]
public string SiteName { get; set; }
/// <summary>
/// 站点域名
/// </summary>
[DisplayName("站点域名")]
[Description("本站点的域名")]
[DefaultValue("http://Yellbuy.com")]
[DataMember]
public string SiteDomain { get; set; }
/// <summary>
/// 站点标题
/// </summary>
[DisplayName("站点标题")]
[Description("本站点的标题")]
[DefaultValue("Yellbuy")]
[DataMember]
public string SiteTitle { get; set; }
/// <summary>
/// 站点Logo地址
/// </summary>
[DisplayName("站点Logo地址")]
[Description("本站点的Logo地址")]
[DefaultValue("")]
[DataMember]
public string SiteLogoUrl { get; set; }
/// <summary>
/// 站点Banner地址
/// </summary>
[DisplayName("站点Banner地址")]
[Description("本站点的Banner地址")]
[DefaultValue("")]
[DataMember]
public string SiteBannerUrl { get; set; }
/// <summary>
/// 站点版权信息
/// </summary>
[DisplayName("站点版权信息")]
[Description("本站点的版权信息")]
[DefaultValue("© 2010-2015 YELLBY.版权所有")]
[DataMember]
public string SiteCopyRight { get; set; }
/// <summary>
/// 站点关键字
/// </summary>
[DisplayName("站点关键字")]
[Description("本站点的关键字")]
[DefaultValue("")]
[DataMember]
public string SiteKeyword { get; set; }
/// <summary>
/// 站点描述
/// </summary>
[DisplayName("站点描述")]
[Description("本站点的描述信息")]
[DefaultValue("")]
[DataMember]
public string SiteDescription { get; set; }
/// <summary>
/// 站点首页路径
/// </summary>
[DisplayName("站点首页路径")]
[Description("本站点的首页路径")]
[DefaultValue("")]
[DataMember]
public string SiteHomePath { get; set; } #endregion #region 邮件参数 /// <summary>
/// Email地址
/// </summary>
[DisplayName("Email地址")]
[Description("Email地址")]
[DefaultValue("19892257@qq.com")]
[DataMember]
public virtual string EmailAddress { get; set; } /// <summary>
/// Email显示名
/// </summary>
[DisplayName("Email显示名")]
[Description("Email显示名")]
[DefaultValue("Yellbuy")]
[DataMember]
public string EmailDisplayName { get; set; } /// <summary>
/// Email主机名
/// </summary>
[DisplayName("Email主机名")]
[Description("Email主机名")]
[DefaultValue("")]
[DataMember]
public string EmailHost { get; set; } /// <summary>
/// Email端口号
/// </summary>
[DisplayName("Email端口号")]
[Description("Email端口号")]
[DefaultValue()]
[DataMember]
public int EmailPort { get; set; } /// <summary>
/// Email用户名
/// </summary>
[DisplayName("Email用户名")]
[Description("Email用户名")]
[DefaultValue("")]
[DataMember]
public string EmailUsername { get; set; } /// <summary>
/// Email密码
/// </summary>
[DisplayName("Email密码")]
[Description("Email密码")]
[DefaultValue("")]
[DataMember]
public string EmailPassword { get; set; } /// <summary>
/// Email是否使用SSL
/// </summary>
[DisplayName("Email是否使用SSL")]
[Description("Email是否使用SSL")]
[DefaultValue(false)]
[DataMember]
public bool EnableSsl { get; set; } /// <summary>
/// Email是否使用默认证书
/// </summary>
[DisplayName("Email是否使用默认证书")]
[Description("Email是否使用默认证书")]
[DefaultValue(false)]
[DataMember]
public bool EmailUseDefaultCredentials { get; set; } /// <summary>
/// Gets a friendly email account name
/// </summary>
public string GetFriendlyName()
{
if (!String.IsNullOrWhiteSpace(this.EmailDisplayName))
return this.EmailAddress + " (" + this.EmailDisplayName + ")";
return this.EmailAddress;
} #endregion #region 页面参数 /// <summary>
/// 页面缓存类型
/// </summary>
[DisplayName("页面缓存类型")]
[Description("0:不缓存,1:绝对时间失效,2:相对时间失效")]
[DefaultValue()]
[DataMember]
public int PageCacheType { get; set; }
/// <summary>
/// 页面缓存时间
/// </summary>
[DisplayName("页面缓存时间")]
[Description("单位:秒(S)")]
[DefaultValue()]
[DataMember]
public int PageCacheInterval { get; set; }
/// <summary>
/// 内容过滤字符串
/// </summary>
[DisplayName("内容过滤字符串")]
[Description("多个内容使用英文半角“,”符合进行分割")]
[DefaultValue("")]
[DataMember]
public string PageFilter { get; set; }
/// <summary>
/// 允许上传的图片格式
/// </summary>
[DisplayName("允许上传的图片格式")]
[Description("多个内容使用英文半角“,”符合进行分割")]
[DefaultValue(".gif,.jpg,.jpeg,.bmp,.png")]
[DataMember]
public string PageUploadAllowImageFormat { get; set; } public string[] GetUploadAllowImageFormat()
{
if (string.IsNullOrWhiteSpace(PageUploadAllowImageFormat)) return new string[];
return PageUploadAllowImageFormat.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries);
}
/// <summary>
/// 允许上传的图片大小
/// </summary>
[DisplayName("允许上传的图片大小")]
[Description("单位:KB,0表示不限制大小")]
[DefaultValue()]
[DataMember]
public int PageUploadAllowImageSize { get; set; }
/// <summary>
/// 允许上传的音频格式
/// </summary>
[DisplayName("允许上传的音频格式")]
[Description("多个内容使用英文半角“,”符合进行分割")]
[DefaultValue(".mid,.mp3,.wma")]
[DataMember]
public string PageUploadAllowAudioFormat { get; set; }
public string[] GetUploadAllowAudioFormat()
{
if (string.IsNullOrWhiteSpace(PageUploadAllowAudioFormat)) return new string[];
return PageUploadAllowAudioFormat.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
/// <summary>
/// 允许上传的音频大小
/// </summary>
[DisplayName("允许上传的音频大小")]
[Description("单位:KB,0表示不限制大小")]
[DefaultValue()]
[DataMember]
public int PageUploadAllowAudioSize { get; set; }
/// <summary>
/// 允许上传的视频格式
/// </summary>
[DisplayName("允许上传的视频格式")]
[Description("多个内容使用英文半角“,”符合进行分割")]
[DefaultValue(".wmv,.asf,.avi,.mpg,.ram,.rm,.swf")]
[DataMember]
public string PageUploadAllowVideoFormat { get; set; }
public string[] GetUploadAllowVideoFormat()
{
if (string.IsNullOrWhiteSpace(PageUploadAllowVideoFormat)) return new string[];
return PageUploadAllowVideoFormat.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
/// <summary>
/// 允许上传的视频大小
/// </summary>
[DisplayName("允许上传的视频大小")]
[Description("单位:KB,0表示不限制大小")]
[DefaultValue()]
[DataMember]
public int PageUploadAllowVideoSize { get; set; }
/// <summary>
/// 允许上传的文档文件格式
/// </summary>
[DisplayName("允许上传的文档文件格式")]
[Description("多个内容使用英文半角“,”符合进行分割")]
[DefaultValue(".rar,.zip,doc,docx,xls,xlsx,pdf")]
[DataMember]
public string PageUploadAllowDocumentFormat { get; set; }
public string[] GetUploadAllowDocumentFormat()
{
if (string.IsNullOrWhiteSpace(PageUploadAllowDocumentFormat)) return new string[];
return PageUploadAllowDocumentFormat.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
/// <summary>
/// 允许上传的文档文件大小
/// </summary>
[DisplayName("允许上传的文档文件大小")]
[Description("单位:KB,0表示不限制大小")]
[DefaultValue()]
[DataMember]
public int PageUploadAllowDocumentSize { get; set; }
/// <summary>
/// 允许上传的其他文件格式
/// </summary>
[DisplayName("允许上传的其他文件格式")]
[Description("多个内容使用英文半角“,”符合进行分割")]
[DefaultValue(".html,.htm,.css,cshtml,aspx")]
[DataMember]
public string PageUploadAllowFileFormat { get; set; }
public string[] GetUploadAllowFileFormat()
{
if (string.IsNullOrWhiteSpace(PageUploadAllowFileFormat)) return new string[];
return PageUploadAllowFileFormat.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
/// <summary>
/// 允许上传的其他文件大小
/// </summary>
[DisplayName("允许上传的文件大小")]
[Description("单位:KB,0表示不限制大小")]
[DefaultValue()]
[DataMember]
public int PageUploadAllowFileSize { get; set; }
#endregion #region 用户设置 /// <summary>
/// 后台登录使用验证码
/// </summary>
[DisplayName("后台登录使用验证码")]
[Description("后台登录使用验证码")]
[DefaultValue(false)]
[DataMember]
public bool AdminUseAuthCode { get; set; } /// <summary>
/// 允许用户注册
/// </summary>
[DisplayName("允许用户注册")]
[Description("是否允许用户注册")]
[DefaultValue(false)]
[DataMember]
public bool UserAllowRegister { get; set; }
/// <summary>
/// 用户注册是否需要使用Email验证
/// </summary>
[DisplayName("用户注册是否需要使用Email验证")]
[Description("用户注册是否需要使用Email验证")]
[DefaultValue(false)]
[DataMember]
public bool UserUseEmailValidate { get; set; }
/// <summary>
/// 用户注册时是否使用验证码
/// </summary>
[DisplayName("用户注册时是否使用验证码")]
[Description("用户注册时是否使用验证码")]
[DefaultValue(false)]
[DataMember]
public bool UserRegisterUseAuthCode { get; set; }
/// <summary>
/// 用户登录时是否使用验证码
/// </summary>
[DisplayName("用户登录时是否使用验证码")]
[Description("用户登录时是否使用验证码")]
[DefaultValue(false)]
[DataMember]
public bool UserLoginUseAuthCode { get; set; }
/// <summary>
/// 用户回复方式
/// </summary>
[DisplayName("用户回复方式")]
[Description("0:不允许回复,1:允许匿名用户回复,2:仅允许登录用户回复")]
[DefaultValue()]
[DataMember]
public int UserReplyKind { get; set; }
/// <summary>
/// 用户回复时是否输入验证码
/// </summary>
[DisplayName("用户回复时是否输入验证码")]
[Description("用户回复时是否输入验证码")]
[DefaultValue(false)]
[DataMember]
public bool UserReplyUseAuthCode { get; set; }
/// <summary>
/// 用户回复时是否加载编辑器
/// </summary>
[DisplayName("用户回复时是否加载编辑器")]
[Description("用户回复时是否加载编辑器")]
[DefaultValue(false)]
[DataMember]
public bool UserReplyUseEditor { get; set; }
/// <summary>
/// 用户回复内容是否需要审核才允许发布
/// </summary>
[DisplayName("用户回复是否需要审核")]
[Description("用户回复内容是否需要审核才允许发布")]
[DefaultValue(false)]
[DataMember]
public bool UserReplyAudit { get; set; } #endregion #region 水印/缩放设置 /// <summary>
/// 水印类型
/// </summary>
[DisplayName("水印类型")]
[Description("水印类型,0:文字水印,1:图片水印")]
[DefaultValue()]
[DataMember]
public int WatermarkKind { get; set; }
/// <summary>
/// 文字水印内容
/// </summary>
[DisplayName("文字水印内容")]
[Description("文字水印内容")]
[DefaultValue("YELLBUY")]
[DataMember]
public string WatermarkText { get; set; }
/// <summary>
/// 文字水印字体大小
/// </summary>
[DisplayName("文字水印字体大小")]
[Description("单位:pt")]
[DefaultValue()]
[DataMember]
public int WatermarkFontSize { get; set; }
/// <summary>
/// 文字水印字体名称
/// </summary>
[DisplayName("文字水印字体名称")]
[Description("")]
[DefaultValue("Arial")]
[DataMember]
public string WatermarkFontFamily { get; set; }
/// <summary>
/// 文字水印字体颜色
/// </summary>
[DisplayName("文字水印字体颜色")]
[Description("")]
[DefaultValue("#Blue")]
[DataMember]
public string WatermarkFontColor { get; set; }
/// <summary>
/// 图片水印的图片地址
/// </summary>
[DisplayName("图片水印的图片地址")]
[Description("")]
[DefaultValue("")]
[DataMember]
public string WatermarkImgUrl { get; set; }
/// <summary>
/// 图片水印的高度
/// </summary>
[DisplayName("图片水印的高度")]
[Description("单位:px")]
[DefaultValue()]
[DataMember]
public int WatermarkImgHeight { get; set; }
/// <summary>
/// 图片水印的宽度
/// </summary>
[DisplayName("图片水印的宽度")]
[Description("单位:px")]
[DefaultValue()]
[DataMember]
public int WatermarkImgWidth { get; set; }
/// <summary>
/// 图片水印的透明度
/// </summary>
[DisplayName("图片水印的透明度")]
[Description("在0%至100%之间,100%表示不透明,0%表示完全透明")]
[DefaultValue(0.5f)]
[DataMember]
public float WatermarkImgOpacity { get; set; }
/// <summary>
/// 图片水印的位置
/// </summary>
[DisplayName("图片水印的位置")]
[Description("0:左上角,1:右上角,2:左下角,3:右下角")]
[DefaultValue()]
[DataMember]
public int WatermarkImgRegion { get; set; } #endregion
}
}

CMS参数设置

数据访问方面,仅需调用SettingApi.LoadSettings即可进行上述参数设置信息的加载,使用SettingApi.SaveSettings方法则进行参数设置信息的保存,最终的参数设置信息将保存至数据库中。数据访问的具体代码如下(注:如下代码中对CMS参数设置信息进行了内存缓存处理以提高系统性能):

     public CmsSetting Load()
{
string key = CACHE_PATTERN_KEY;
return _cacheManager.Get(key, () =>
{
var pv = SettingApi.LoadSettings<CmsSetting>();
return pv;
});
} public void Save(CmsSetting setting)
{
if (setting == null)
throw new ArgumentNullException("setting"); SettingApi.SaveSettings(setting);
//移除缓存
_cacheManager.RemoveByPattern(CACHE_PATTERN_KEY);
}

CMS参数设置的数据访问方法

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配置如下:

 this.HasMany(p => p.CmsColumn).WithMany().Map(pc =>
{
pc.ToTable("YbCmsRelation");
pc.MapLeftKey(new string[] { "DataId", "Draft" });
pc.MapRightKey("RelatedId");
});

上述代码中,使用了一个关联表"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进行了必要的重写,以让其适应新的需求;在具体过程中,还实现了对图片的优化处理,例如可按图片请求的尺寸生成对应尺寸的缩略图并进行缓存处理等。

生成缩略图的代码如下:

 public static Image GetThumbnail(Image img, int size)
{
// 生成缩略图
var bmp = new Bitmap(size, size);
using (var grp = Graphics.FromImage(bmp))
{
grp.SmoothingMode = SmoothingMode.HighQuality;
grp.CompositingQuality = CompositingQuality.HighQuality;
grp.InterpolationMode = InterpolationMode.High; // Resize and crop image
var dst = new Rectangle(, , bmp.Width, bmp.Height);
grp.DrawImage(img, dst, img.Width > img.Height ? (img.Width - img.Height)/ : ,
img.Height > img.Width ? (img.Height - img.Width)/ : , Math.Min(img.Width, img.Height),
Math.Min(img.Height, img.Width), GraphicsUnit.Pixel);
grp.Dispose();
}
return bmp;
}

生成缩略图

总结:目前国内.NET平台的 CMS 大部分均以 ASP.NET WebForm 为主,其实以CMS的特点来看,使用 ASP.NET MVC 进行开发无疑更加容易上手,开发和维护也更加方便和快捷。本CMS系统将在现有版本的基础之上提供更多符合国内使用习惯的功能和模块,具体可访问http://pjdemo.yellbuy.com/进一步了解。