• YbSoftwareFactory 代码生成插件【二十二】:CMS基础功能的实现


       很多网友建议在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 }
    CMS参数设置

       数据访问方面,仅需调用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         }
    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配置如下:

    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/进一步了解。

  • 相关阅读:
    文档搜索引擎 vofill
    angularjs搭建项目步骤 vofill
    KlipperBox:帮助新手快速上手 Klipper 固件(上位机系统)
    在任意地方拿到SpringBoot管理的类(也称@Autowired拿到的值为空)
    SpringBoot获取客户端请求的IP
    vue.config.js中关于css.loaderOptions作用和配置
    git 协同开发常用命令
    scss 中的指令@import 、@media 、@extend、@mixin 、@include、 占位符%
    vue项目中容器布局自动撑满和盒子内容超出隐藏滚动条
    vue中vuecil3关于eslint告警0 errors and 7 warnings potentially fixable with the `fix` option.
  • 原文地址:https://www.cnblogs.com/gyche/p/4320772.html
Copyright © 2020-2023  润新知