Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之文件上传

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/tianxiaode/article/details/79048253
————————————————
版权声明:本文为CSDN博主「上将军」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/tianxiaode/article/details/79048253

媒体管理的主要难度就在于媒体文件的上传,由于使用了plupload作为上传工具,因而整个实现流程还是比较简单。

由于媒体实体没有大文本数据,因而GetGetAll方法使用相同的数据传输对象就可以,具体代码如下:

    [AutoMapFrom(typeof(Media))]
    public class MediaDto: EntityDto<long>
    {
        public string Filename { get; set; }

        public string Description { get; set; }

        public string Path { get; set; }

        public MediaType Type { get; set; }

        public int Size { get; set; }

        public DateTime CreationTime { get; set; }
    }
 
 

对于GetAll方法,需要提供自定义排序和查询等功能,因而需自定义输入对象,代码如下:

    public class GetAllMediaInputDto : PagedAndSortedResultRequestDto, IShouldNormalize
    {
        private readonly JObject _allowSorts = new JObject()
        {
            { "creationTime", "CreationTime" },
            { "size", "Size" },
            { "description", "Description" }
        };

        public string Query { get; set; }
        public int? Year { get; set; }
        public int? Month { get; set; }
        public int? Day { get; set; }
        [Required]
        public int[] Type { get; set; }
        public string Sort { get; set; }

        public void Normalize()
        {
            if (!string.IsNullOrEmpty(Sort))
            {
                Sorting = Helper.ExtJs.OrderBy(Sort, _allowSorts);
            }
        }
    }
 
 

plupload是一个个文件上传的,因而上传所用的数据传输对象,只接收一个文件就行了,代码如下:

    public class CreateMediaDto
    {
        [Required]
        public IFormFile File { get; set; }
    }
 
 

要注意的是Asp.Net Core是接收文件是是否IFormFile对象,不是使用HttpPostedFileBase

媒体的描述字段(Description)允许更新,因而要写一个更新对象,代码如下:

    [AutoMapTo(typeof(Media))]
    public class UpdateMediaInputDto : EntityDto<long>
    {
        [MaxLength(Media.MaxDescriptionLength)]
        public string Description { get; set; }
    }
 
 

删除还是采取一次可删除多个记录的方式,需要重定义输入接口,代码如下:

    public class DeleteMediaInputDto
    {
        public long[] Id { get; set; }
    }
 
 

感觉这个可以做成一个通用类,就不用写n个那么麻烦了。

数据传输对象完成后,先定义一个服务接口,代码如下:

    public interface IMediaAppService :IAsyncCrudAppService<MediaDto, long>
    {

    }
 
 

最后是完成服务类,代码如下:

    [AbpAuthorize(PermissionNames.Pages_Articles)]
    public class MediaAppService: AsyncCrudAppService<Media, MediaDto, long, GetAllMediaInputDto, CreateMediaDto, UpdateMediaInputDto, MediaDto>
    {
        public MediaAppService(IRepository<Media, long> repository) : base(repository)
        {
        }

        public override async Task<PagedResultDto<MediaDto>> GetAll(GetAllMediaInputDto input)
        {
            CheckGetAllPermission();
            var query = Repository.GetAll().Where(m=>input.Type.Contains((int)m.Type));

            if (!string.IsNullOrEmpty(input.Query)) query = query.Where(m => m.Description.Contains(input.Query));
            if (input.Year != null && input.Month != null)
            {
                query = query.Where(m => m.CreationTime.Year == input.Year && m.CreationTime.Month == input.Month);
            }
            if (input.Day != null)
            {
                query = query.Where(m => m.CreationTime.Day == input.Day);
            }

            var totalCount = await AsyncQueryableExecuter.CountAsync(query);

            query = ApplySorting(query, input);
            query = ApplyPaging(query, input);

            var entities = await AsyncQueryableExecuter.ToListAsync(query);
            return new PagedResultDto<MediaDto>(
                totalCount,
                entities.Select(MapToEntityDto).ToList()
            );

        }

        public override async Task<MediaDto> Create([FromForm]CreateMediaDto input)
        {
            CheckCreatePermission();
            var stream = input.File.OpenReadStream();
            var allowImageFileType = await SettingManager.GetSettingValueAsync(AppSettingNames.AllowImageFileType);
            var allowAudioFileType = await SettingManager.GetSettingValueAsync(AppSettingNames.AllowAudioFileType);
            var allowVideoFileType = await SettingManager.GetSettingValueAsync(AppSettingNames.AllowVideoFileType);
            var allowUploadSize = await SettingManager.GetSettingValueAsync<int>(AppSettingNames.AllowUploadSize);
            var fileType = stream.GetFileType();
            var ext = fileType?.Extension;
            MediaType? type = null;
            if (allowImageFileType.IndexOf($",{ext},", StringComparison.OrdinalIgnoreCase) >= 0)
            {
                type = MediaType.Image;

            }
            else if (allowAudioFileType.IndexOf($",{ext},", StringComparison.OrdinalIgnoreCase) >= 0)
            {
                type = MediaType.Audio;
            }
            else if (allowVideoFileType.IndexOf($",{ext},", StringComparison.OrdinalIgnoreCase) >= 0)
            {
                type = MediaType.Video;
            }
            if (type == null) throw new UserFriendlyException("fileTypeNotAllow");
            if(stream.Length ==0 || stream.Length>allowUploadSize ) throw new UserFriendlyException("fileSizeNotAllow");
            var filename = ShortGuid.ToShortGuid(Guid.NewGuid());
            var path = $"{filename.Substring(0, 2)}/{filename.Substring(2, 2)}";
            var dir = $"{Environment.CurrentDirectory}\\wwwroot\\upload\\{path}";
            if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
            using (var fileStream = new FileStream($"{dir}\\{filename}.{ext}", FileMode.Create))
            {
                await input.File.CopyToAsync(fileStream);
            }
            if (type == MediaType.Image)
            {
                using (var image = Image.Load<Rgba32>($"{dir}\\{filename}.{ext}"))
                {
                    image.Mutate(x => x
                        .Resize(160, 160));
                    image.Save($"{dir}\\thumbnail_{filename}.{ext}");
                }
            }
            var entity = new Media()
            {
                Filename = $"{filename}.{ext}",
                Description = input.File.FileName,
                Size = (int)stream.Length,
                Path = path,
                Type = type ?? MediaType.Image,
                CreatorUserId = AbpSession.UserId,
                TenantId = AbpSession.TenantId ?? 1
            };
            await Repository.InsertAsync(entity);
            await CurrentUnitOfWork.SaveChangesAsync();
            return MapToEntityDto(entity);

        }


        public async Task Delete(DeleteMediaInputDto input)
        {
            CheckDeletePermission();
            foreach (var inputId in input.Id)
            {
                await Repository.DeleteAsync(inputId);
            }

        }

        public async Task<ListResultDto<ComboBoxItemDto>> GetDateList()
        {
            CheckGetAllPermission();
            var query = await Repository.GetAllListAsync();
            var list = from media in query
                let year = media.CreationTime.Year
                let month = media.CreationTime.Month
                group media by new {year, month}
                into g
                orderby g.Key.year descending, g.Key.month descending
                select new ComboBoxItemDto
                {
                    Id = g.Key.year.ToString() + "," + g.Key.month.ToString(),
                    Text = g.Key.year.ToString() + "年" + g.Key.month.ToString() + "月"
                };
            var results =
                new List<ComboBoxItemDto> {new ComboBoxItemDto("all", "全部"), new ComboBoxItemDto("today", "今天")};
            var itemDtos = results.Union(list.ToList());
            return new ListResultDto<ComboBoxItemDto>(itemDtos.ToList());
        }


        [NonAction]
        public override Task Delete(EntityDto<long> input)
        {
            return base.Delete(input);
        }
    }

 
 

Create方法内,使用到了ABP框架的设置管理(SettingManager),这个,我们需要在Core项目的Configuration文件夹下定义,首先要在AppSettingNames类中定义一些常量,代码如下:

    public static class AppSettingNames
    {
        public const string UiTheme = "App.UiTheme";
        public const string AllowImageFileType = "App.AllowImageFileType";
        public const string AllowAudioFileType = "App.AllowAudioFileType";
        public const string AllowVideoFileType = "App.AllowVideoFileType";
        public const string AllowUploadSize = "App.AllowUploadSize";
    }
 
 

这里的定义参考常量UiTheme的定义来进行就行了。接下来是在AppSettingProvider中添加设置,具体代码如下:

    public class AppSettingProvider : SettingProvider
    {
        public override IEnumerable<SettingDefinition> GetSettingDefinitions(SettingDefinitionProviderContext context)
        {
            return new[]
            {
                new SettingDefinition(AppSettingNames.UiTheme, "red", scopes: SettingScopes.Application | SettingScopes.Tenant | SettingScopes.User, isVisibleToClients: true),
                new SettingDefinition(AppSettingNames.AllowImageFileType,",jpg,gif,png,jpeg,",scopes:SettingScopes.All,isVisibleToClients:true),
                new SettingDefinition(AppSettingNames.AllowAudioFileType,",mp3,flac,wav,",scopes:SettingScopes.All,isVisibleToClients:true),
                new SettingDefinition(AppSettingNames.AllowVideoFileType,",mp4,m4v,flv,mov,",scopes:SettingScopes.All,isVisibleToClients:true),
                new SettingDefinition(AppSettingNames.AllowUploadSize,"10485760",scopes:SettingScopes.All,isVisibleToClients:true),
            };
        }
    }
 
 

有关SettingDefinition的具体信息可参考文档《设置管理》。

如果希望将设置定义在appsettings.json中,可以在Web.Core项目中添加一个从SettingProvider派生的类,然后在GetSettingDefinitions方法内从appsettings.json读取所需的配置再。最后调用Configuration.Settings.Providers.Add方法添加配置。

在获取到允许的文件类型和文件大小后,就可通过Mime-Detective包提供的扩展方法GetFileType来获取文件的实际类型了。如果实际的扩展名在允许的文件类型内,则设置媒体文件的类型。如果文件的大小也在允许范围内,则调用ShortGuid.ToShortGuid方法来生成一个文件名,并将文件名的前两个字符作为一级路径,第3、4位字符作为一个路径。在应用服务内,使用Environment.CurrentDirectory来获取当前工作目录比较简单,也不需要做转换,可抛弃MapPath方法。对于Asp.Net Core项目,直接访问路径是在wwwroot文件夹,不能直接方在工作目录的根文件夹,这个一定要注意。创建存放文件夹后,就可调用CopyToAsync方法将文件保存了。

由于ImageResizer包不支持Asp.Net Core,而后续项目imageflow虽然有测试版,但完全没文档说明如何去使用,只有暂时放下这东西,乖乖的创建一个缩略图了。在寻找可支持Asp.Net Core的图像处理库的时候,找到了《.NET Core Image Processing.NET Core Image Processing这篇文章,介绍了很多处理图片的库,最终选择了SixLabors.ImageSharp这个库。选择这个库的主要原因是不需要其他库的支持也可以支持Window和Linux环境,比较方便。

在保存缩略图后,就可创建实体并添加到数据库了,最后返回实体。

在媒体管理视图中,有个一个查询是根据日期来查询媒体的,而这需要服务器端返回根据年和月分组后的日期,因而需要创建一个GetDateList方法。在方法内,使用了根据前端需要定义的一个下拉列表类,具体代码如下:

    [Serializable]
    public class ComboBoxItemDto
    {
        public string Text { get; set; }

        public string Id { get; set; }


        public ComboBoxItemDto()
        {

        }

        public ComboBoxItemDto(string id, string text)
        {
            Text = text;
            Id = id;
        }

    }
 
 

这个类是根据框架提供的下拉列表类修改属性后创建的。区域其实不大,但方便Ext JS使用。

GetDateList方法内先通过分组创建一个ComboBoxItemDto列表,然后与额外的两个选项合并后返回客户端。

至此,后台代码就完成了,下面来完成客户端代码。

在客户端先把模型等与字段相关的地方修改好。接下来要修改的是视图模型内datelists存储的访问地址,需要将地址datelist修改为getdatelist

最关键的修改是要在视图控制器的onBeforeUpload方法内,为 plupload的请求添加认证头,代码如下:

    onBeforeUpload: function (cmp, uploader, file) {
        var me = this,
            tb = me.lookupReference('progressToolBar'),
            progress = tb.down('progressbar');
        uploader.setOption('headers',Ext.apply( {}, HEADERS.getHeaders()));
        progress.setValue(0);
        progress.updateText(Ext.String.format(I18N.Uploading, file.name, 0));
        tb.show();
    },
 
 

plupload提供了setOption方法来修改选择,把headers选项加进去就行了,难度不大。如果不使用plupload,而是使用Ajax提交,可以参考这个说明:https://stackoverflow.com/questions/37258612/upload-document-to-web-api

余下要修改的是onDescriptionEditComplete方法,为Ajax请求配置methodPUT,将params修改为jsonData.。

至此,文件上传功能就已经完成了。

项目源代码:https://gitee.com/tianxiaode

  •                     <li class="tool-item tool-active is-like "><a href="javascript:;"><svg class="icon" aria-hidden="true">
                            <use xlink:href="#csdnc-thumbsup"></use>
                        </svg><span class="name">点赞</span>
                        <span class="count"></span>
                        </a></li>
                        <li class="tool-item tool-active is-collection "><a href="javascript:;" data-report-click="{&quot;mod&quot;:&quot;popu_824&quot;}"><svg class="icon" aria-hidden="true">
                            <use xlink:href="#icon-csdnc-Collection-G"></use>
                        </svg><span class="name">收藏</span></a></li>
                        <li class="tool-item tool-active is-share"><a href="javascript:;"><svg class="icon" aria-hidden="true">
                            <use xlink:href="#icon-csdnc-fenxiang"></use>
                        </svg>分享</a></li>
                        <!--打赏开始-->
                                                <!--打赏结束-->
                                                <li class="tool-item tool-more">
                            <a>
                            <svg t="1575545411852" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5717" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M179.176 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5718"></path><path d="M509.684 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5719"></path><path d="M846.175 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5720"></path></svg>
                            </a>
                            <ul class="more-box">
                                <li class="item"><a class="article-report">文章举报</a></li>
                            </ul>
                        </li>
                                            </ul>
                </div>
                            </div>
            <div class="person-messagebox">
                <div class="left-message"><a href="https://blog.csdn.net/tianxiaode">
                    <img src="https://profile.csdnimg.cn/7/E/6/3_tianxiaode" class="avatar_pic" username="tianxiaode">
                                            <img src="https://g.csdnimg.cn/static/user-reg-year/1x/19.png" class="user-years">
                                    </a></div>
                <div class="middle-message">
                                        <div class="title"><span class="tit"><a href="https://blog.csdn.net/tianxiaode" data-report-click="{&quot;mod&quot;:&quot;popu_379&quot;}" target="_blank">上将军</a></span>
                                                    <span class="flag expert">
                                <a href="https://blog.csdn.net/home/help.html#classicfication" target="_blank">
                                    <svg class="icon" aria-hidden="true">
                                        <use xlink:href="#csdnc-blogexpert"></use>
                                    </svg>
                                    博客专家
                                </a>
                            </span>
                                            </div>
                    <div class="text"><span>发布了179 篇原创文章</span> · <span>获赞 153</span> · <span>访问量 176万+</span></div>
                </div>
                                <div class="right-message">
                                            <a href="https://bbs.csdn.net/topics/395531463" target="_blank" class="btn btn-sm btn-red-hollow bt-button personal-messageboard">他的留言板
                        </a>
                                                            <a class="btn btn-sm  bt-button personal-watch" data-report-click="{&quot;mod&quot;:&quot;popu_379&quot;}">关注</a>
                                    </div>
                            </div>
                    </div>
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值