一、项目多层架构搭建

1、项目环境

这里我们会新创建一个空白的解决方案。

创建对应的项目,以及接口项目

2、实体类创建

Cms.Entity这个类库项目中创建BaseEntity.cs实体类,封装公共的属性,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Cms.Entity
{
public class BaseEntity<TKey>
{
/// <summary>
/// 主键id
/// </summary>
public TKey Id { get; set; }

/// <summary>
/// 添加数据时间
/// </summary>
public DateTime CreateTime { get; set; }
/// <summary>
/// 更新数据时间
/// </summary>
public DateTime UpdateTime { get; set; }
/// <summary>
/// 软删除
/// </summary>
public bool Del工作单Flag { get; set; }
}
}

同时创建一个UserInfo.cs实体类,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Cms.Entity
{
public class UserInfo:BaseEntity<long>
{
/// <summary>
/// 用户名
/// </summary>
public string? UserName { get; set; }
/// <summary>
/// 密码
/// </summary>
public string? UserPassword { get; set;}
/// <summary>
/// 邮箱
/// </summary>
public string? UserEmail { get; set;}
/// <summary>
/// 电话
/// </summary>
public string? UserPhone { get; set;}

/// <summary>
/// 性别:0表示男,1表示女
/// </summary>
public int Gender { get; set;}
/// <summary>
/// 用户头像,存储头像路径
/// </summary>
public string? PhotoUrl { get; set;}
}
}

创建一个UserInfoConfig.cs类,进行配置,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Cms.Entity
{

public class UserInfoConfig : IEntityTypeConfiguration<UserInfo>
{
public void Configure(EntityTypeBuilder<UserInfo> builder)
{
builder.ToTable("T_UserInfos");
builder.Property(x=>x.UserName).HasMaxLength(20).IsRequired();
builder.Property(x=>x.UserPassword).HasMaxLength(20).IsRequired();
builder.Property(x => x.UserEmail).HasMaxLength(100).IsRequired();
builder.Property(x=>x.UserPhone).HasMaxLength(16).IsRequired();
builder.Property(x => x.Gender).HasDefaultValue(0);
builder.Property(x => x.PhotoUrl).HasMaxLength(100);
}

}
}

在当前的Cms.Entity类库项目中,需要安装的包:

1
2
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.4"></PackageReference>

3、DbContext搭建

Cms.EntityFrameworkCore项目中安装EFCore所需要的包

1
2
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools

注意:在程序包管理器控制台中选择的项目是Cms.EntityFrameworkCore

同时在该项目中创建DbContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MyDbContext:DbContext
{
public DbSet<UserInfo>UserInfos { get; set; }
public MyDbContext(DbContextOptions<MyDbContext>options):base(options)
{
// 这里添加构造方法的目的:就是对当前的MyDbContext进行注入,并且从配置文件中读取数据库链接配置

}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 通过反射机制加载程序集(AppDomain.CurrentDomain.BaseDirectory:当前程序集的基目录)
var assembly = Assembly.LoadFrom(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Cms.Entity.dll"));
// 获取此程序集中定义的所有类型。
var types = assembly.GetTypes();
foreach ( var type in types ) {
modelBuilder.ApplyConfigurationsFromAssembly(type.Assembly); // 执行`Cms.Entity`程序集中实现了IEntityTypeConfiguration接口的配置
}


}
}

在当前的Cms.EntityFrameworkCore的类库项目中引入Cms.Entity项目

Cms.WebUI这个Web项目中引入Cms.EntityFrameworkCoreCms.Entity项目

同时在Web项目中的appsettings.json文件中,添加数据库的链接字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"StrConn": "server=localhost;database=CmsDb;uid=sa;pwd=123456;TrustServerCertificate=true" // 数据库链接字符串
}
}

Program.cs文件中,对DbContext进行注入

1
2
3
4
5
6
7
8
builder.Services.AddControllersWithViews();
//注入DbContext,同时获取数据库链接字符串
builder.Services.AddDbContext<MyDbContext>(opt =>
{
string connStr = builder.Configuration.GetConnectionString("StrConn")!;
opt.UseSqlServer(connStr);
});
var app = builder.Build();

同时在CMS.WebUI这个UI项目中,也需要安装如下包:

1
2
3
4
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

下面生成数据库迁移

在【程序包管理器控制台】中选择Cms.EntityFrameworkCore这个类库项目

然后执行如下迁移命令(需要将CMS.WebUI项目设置为启动项目,同时将以上安装的各个库的版本保持一致)

1
2
Add-Migration CreateUserInfo
Update-database

完成迁移以后,查看数据库。

4、数据仓储设计

4.1 仓储接口设计

Cms.IRepository项目中(在该项目中需要引用Cms.Entity类库项目),创建IUserInfoRepository.cs接口,该接口中的代码如下所示:

1
2
3
4
5
6
7
8
public interface IUserInfoRepository
{
IQueryable<UserInfo> LoadEntities(Expression<Func<UserInfo,bool>>wehreLambda);
IQueryable<UserInfo> LoadPageEntities(int pageIndex, int pageSize, out int totalCount, Expression<Func<UserInfo, bool>> whereLambda, Expression<Func<UserInfo, int>> orderbyLambda, bool isAsc);
Task<bool> DeleteEntityAsync(UserInfo entity);
Task<bool> UpdateEntityAsync(UserInfo entity);
Task<UserInfo> InsertEntityAsync(UserInfo entity);
}

我们可以看到了在这个接口中,定义了针对用户信息的增删改查的方法。

但是有一个问题,其它的接口中也有这些方法,所以这里我们需要做进一步的进行封装的处理。

Cms.IRepository中在创建一个基接口的仓储IBaseRepository.cs,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
namespace Cms.IRepository
{
public interface IBaseRepository<T>where T : class,new()
{
IQueryable<T> LoadEntities(Expression<Func<T, bool>> wehreLambda);
IQueryable<T> LoadPageEntities<S>(int pageIndex, int pageSize, out int totalCount, Expression<Func<T, bool>> whereLambda, Expression<Func<T, S>> orderbyLambda, bool isAsc);
Task<bool> DeleteEntityAsync(T entity);
Task<bool> UpdateEntityAsync(T entity);
Task<T> InsertEntityAsync(T entity);
}
}

这里我们使用了泛型。

IUserInfoRepository接口要继承IBaseRepository这个接口。

1
2
3
4
public interface IUserInfoRepository:IBaseRepository<UserInfo>
{
// 这里定义的是`IUserInfoRepository`这个接口自己独有的方法。
}

4.2 实现仓储接口1

仓库接口的具体实现是在Cms.Repository这个类库项目中完成的。

在该类库项目中先将CMS.EntityCms.IRepository这两个项目引入进来。

然后创建UserInfoRepository.cs这个具体的用户信息仓储类。它应该实现自己的IUserInfoRepository接口

如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class UserInfoRepository : IUserInfoRepository
{

public Task<bool> DeleteEntityAsync(UserInfo entity)
{
throw new NotImplementedException();
}

public Task<UserInfo> InsertEntityAsync(UserInfo entity)
{
throw new NotImplementedException();
}

public IQueryable<UserInfo> LoadEntities(Expression<Func<UserInfo, bool>> wehreLambda)
{
throw new NotImplementedException();
}

public IQueryable<UserInfo> LoadPageEntities<s>(int pageIndex, int pageSize, out int totalCount, Expression<Func<UserInfo, bool>> whereLambda, Expression<Func<UserInfo, s>> orderbyLambda, bool isAsc)
{
throw new NotImplementedException();
}

public Task<bool> UpdateEntityAsync(UserInfo entity)
{
throw new NotImplementedException();
}
}

当看到以上代码的时候,我们就想到了,这些代码也是公共的代码,也应该进行进一步的封装处理。

这时候,再创建BaseRepository.cs这个基仓储,其中的代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
namespace Cms.Repository
{
public class BaseRepository<T> where T : class,new()
{
public Task<bool> DeleteEntityAsync(T entity)
{
throw new NotImplementedException();
}

public Task<T> InsertEntityAsync(T entity)
{
throw new NotImplementedException();
}

public IQueryable<T> LoadEntities(Expression<Func<T, bool>> wehreLambda)
{
throw new NotImplementedException();
}

public IQueryable<T> LoadPageEntities<s>(int pageIndex, int pageSize, out int totalCount, Expression<Func<T, bool>> whereLambda, Expression<Func<T, s>> orderbyLambda, bool isAsc)
{
throw new NotImplementedException();
}

public Task<bool> UpdateEntityAsync(T entity)
{
throw new NotImplementedException();
}
}
}

上面的BaseRepository类实现了泛型。

同时封装的就是公共的代码的实现。

下面修改UserInfoRepository.cs类中的代码

1
2
3
4
5
public class UserInfoRepository :BaseRepository<UserInfo>, IUserInfoRepository
{

// 这里实现的是`UserInfoRepository`独有的方法。
}

可以看到这里继承了BaseRepository,传递的泛型的类型是UserInfo这个类型。

下面要考虑的就是在BaseRepository类中所定义的这些公共方法的具体实现。

这里要实现这些方法,就需要将DbContext注入进来。

修改BaseRepository.cs类,代码如下所示:

1
2
3
4
5
6
7
8
public class BaseRepository<T> where T : class,new()
{
// 注入DbContext
protected readonly MyDbContext ctx;
public BaseRepository(MyDbContext ctx)
{
this.ctx = ctx;
}

这里我们在BaseRepository.cs类中注入了MyDbContext

这里注意:关于ctx声明,我们使用了protected,原因就是在子类UserInfoRepository.cs中我们也要使用ctx这个MyDbContext,因为在UserInfoRepository实现的是它自己独有的方法,而这些方法在操作数据的时候也需要使用到ctx这个MyDbContext.

下面再对UserInfoRepository.cs这个子类中的代码做一下修改

1
2
3
4
5
public class UserInfoRepository :BaseRepository<UserInfo>, IUserInfoRepository
{
// 由于父类BaseRepository的构造方法中,需要MyDbContext,所以这里子类的构造函数中添加MyDbContext参数,最终会传递给父类的构造函数,这样才真正的完成了MyDbContext的注入操作。
public UserInfoRepository(MyDbContext context):base(context) { }
}

下面要实现的就是BaseRepository.cs中具体的方法中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BaseRepository<T> where T : class,new()
{
// 注入DbContext
protected readonly MyDbContext ctx;
public BaseRepository(MyDbContext ctx)
{
this.ctx = ctx;
}
// 实现删除操作
public async Task<bool> DeleteEntityAsync(T entity)
{
ctx.Set<T>().Remove(entity);
return await ctx.SaveChangesAsync()>0;
}

在上面的代码中,我们先实现了删除的方法,但是实现这个方法以后,我们发现了一个问题:就是每次调用删除或者是更新的时候都需要执行SaveChangesAsync方法,完成数据的操作。但是在某些业务场景下,我们只希望提交一次数据。

例如:一个请求过来,把张三这个学生的总分成绩加10分,同时将李四学生的成绩减去5分,如果按照我们现在的设计,需要提交两次数据库。而为了提升性能,我们针对当前的请求其实只需要向数据库发送一次请求就可以了。

要达到这样的效果,就需要使用工作单元模式。

4.3 工作单元的实现

我们知道,要想把数据保存,更新到数据库中,我们必须要调用SaveChanges方法。

而且一个请求过来以后,首先到达控制器中的方法,在控制器的方法中调用了业务,业务最终调用了数据仓储,当这些操作完成后,我们就可以统一调用SaveChangeAsync方法,完成数据的操作。

这里我们可以使用Filter过滤器来帮我们实现。好处是不需要我们自己手动调用SaveChangeAsync方法,如果将工作单元的代码封装到一个类中,还需要我们手动调用。

我们将代码定义在Filter中的好处就是,当控制器方法执行完毕后自动调用SaveChangeAsync方法。

当然,如果通过Filter来完成数据的操作,就会涉及到一个问题,所有控制器中的方法执行完后,都会执行ActionFiler.

为了避免这个问题,这里我们可以创建一个Attribute,只有给控制器的方法上添加了该Attribute,才会执行ActionFiler完成数据的操作。

Cms.WebUI项目中创建Attributes目录,在该目录下面创建UnitOfWorkAttribute.cs类,该类的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
namespace Cms.WebUI.Attributes
{
[AttributeUsage(AttributeTargets.Method)] // 这里的Attribute只能用于方法中
public class UnitOfWorkAttribute:Attribute
{
public Type[] DbContextTypes { get; set; }// 考虑到项目中可能有多个DbContext,所以定义了一个数组
public UnitOfWorkAttribute(Type[] dbContextTypes) {
this.DbContextTypes = dbContextTypes;
}
}
}

下面在Cms.WebUI中创建一个Filters文件夹,在该文件夹下面创建UnitOfWorkFilter.cs

该类的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
namespace Cms.WebUI.Filters
{
public class UnitOfWorkFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var result = await next();// 执行完控制器中的方法了
if (result.Exception != null)
{
return;// 如果该条件成立,表示控制器中方法执行出现了错误,所以就没有必要执行SaveChanges方法了
}
// 下面要获取添加在控制器方法上的Attribute
var actionDesc = context.ActionDescriptor as ControllerActionDescriptor;
if (actionDesc == null)
{
return;// 表示无法转换,也会终止
}
var unAttr = actionDesc.MethodInfo.GetCustomAttribute<UnitOfWorkAttribute>();
if (unAttr == null)
{
return;// 如果该条件成立,表示所执行的控制器的方法上没有添加UnitOfWorkAttribute,这里就会终止,没有必要在执行下面的SaveChanges方法了
}
// 从UnitOfWorkAttribute中的DbContextTypes数组中获取所保存的DbContext
foreach (var dbCxtType in unAttr.DbContextTypes)
{
// 我们知道DbContext都是注入到容器中,所以这里需要从容器中获取
//HttpContext.RequestServices:表示针对当前请求,从容器中获取对应的实例对象
var dbCtx = context.HttpContext.RequestServices.GetService(dbCxtType) as DbContext;
if (dbCtx != null)
{
await dbCtx.SaveChangesAsync();
}
}
}
}
}

下面在Program.cs中注册上面所创建的Filter

1
2
3
4
5
6
7
8
9
10
builder.Services.AddDbContext<MyDbContext>(opt =>
{
string connStr = builder.Configuration.GetConnectionString("StrConn")!;
opt.UseSqlServer(connStr);
});
// 注入UnitOfWorkFilter
builder.Services.Configure<MvcOptions>(c =>
{
c.Filters.Add<UnitOfWorkFilter>();
});

下面做一个简单的测试

Home控制器中添加一个Test方法

如下所示:

1
2
3
4
5
[UnitOfWork(new Type[] {typeof(MyDbContext) })]
public IActionResult Test()
{
return Content("ok");
}

Test方上添加了UnitOfWork这个Attribute.

下面启动项目测试一下,给UnitOfWorkFilter.cs类中的代码打上断点进行调试。

4.4 实现仓储接口2

在这一小节中,我们继续来实现BaseRepository.cs中的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
using Cms.Entity;
using Cms.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;

namespace Cms.Repository
{
public class BaseRepository<T> where T : class,new()
{
// 注入DbContext
protected readonly MyDbContext ctx;
public BaseRepository(MyDbContext ctx)
{
this.ctx = ctx;
}
/// <summary>
/// 删除
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>

public Task<bool> DeleteEntityAsync(T entity)
{
ctx.Set<T>().Remove(entity);
return Task.FromResult<bool>(true);
}
/// <summary>
/// 添加
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>

public Task<T> InsertEntityAsync(T entity)
{
ctx.Set<T>().Add(entity);
return Task.FromResult<T>(entity);
}
/// <summary>
/// 查询
/// </summary>
/// <param name="wehreLambda"></param>
/// <returns></returns>
public IQueryable<T> LoadEntities(Expression<Func<T, bool>> wehreLambda)
{
return ctx.Set<T>().Where<T>(wehreLambda);
}
/// <summary>
/// 分页查询
/// </summary>
/// <typeparam name="s">方法的泛型类型</typeparam>
/// <param name="pageIndex">当前页码</param>
/// <param name="pageSize">每页显示的记录数</param>
/// <param name="totalCount">总的记录数</param>
/// <param name="whereLambda">过滤条件</param>
/// <param name="orderbyLambda">排序条件</param>
/// <param name="isAsc">排序方式</param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public IQueryable<T> LoadPageEntities<S>(int pageIndex, int pageSize, out int totalCount, Expression<Func<T, bool>> whereLambda, Expression<Func<T, S>> orderbyLambda, bool isAsc)
{
var temp = ctx.Set<T>().Where<T>(whereLambda);
totalCount = temp.Count();
if (isAsc) // 升序排序
{
temp = temp.OrderBy<T,S>(orderbyLambda).Skip<T>((pageIndex-1)*pageSize).Take<T>(pageSize);
}
else
{
temp = temp.OrderByDescending<T, S>(orderbyLambda).Skip<T>((pageIndex - 1) * pageSize).Take<T>(pageSize);
}
return temp;
}
/// <summary>
/// 更新
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>

public Task<bool> UpdateEntityAsync(T entity)
{
ctx.Set<T>().Update(entity);
return Task.FromResult<bool>(true);
}
}
}

5、服务层设计

5.1 服务接口设计

Cms.IService项目中(需要在该项目中引入Cms.Entity),创建一个IUserInfoService.cs,针对用户的业务操作。

目前IUserInfoService.cs也是基本的增删改查,这些公共的方法。

同样把公共的业务操作的方法进行封装处理。所以这里在Cms.IService项目中在创建IBaseService.cs这个接口,该接口中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
namespace Cms.IService
{
public interface IBaseService<T>where T:class,new()
{
IQueryable<T> LoadEntities(Expression<Func<T, bool>> wehreLambda);
IQueryable<T> LoadPageEntities<S>(int pageIndex, int pageSize, out int totalCount, Expression<Func<T, bool>> whereLambda, Expression<Func<T, S>> orderbyLambda, bool isAsc);
Task<bool> DeleteEntityAsync(T entity);
Task<bool> UpdateEntityAsync(T entity);
Task<T> InsertEntityAsync(T entity);
}
}

同样的IUserInfoService.cs要继承IBseService.cs

1
2
3
4
5
6
namespace Cms.IService
{
public interface IUserInfoService:IBaseService<UserInfo>
{
}
}

5.2 实现服务接口

Cms.Service这个类库项目中(在该类库项目中引入Cms.Entity,Cms.IService,Cms.IRepository),创建一个UserInfoService.cs这个类.该类的代码如下所示:

1
2
3
4
5
6
7
8
namespace Cms.Service
{
public class UserInfoService :BaseService<UserInfo>,IUserInfoService
{

}
}

UserInfoService.cs这个具体的服务类中要实现用户管理的具体的业务,所以要实现IUserInfoService这个接口,但是公共的业务方法也要进行封装,这里封装到了BaseService中。

BaseSevice.cs这个类中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using Cms.Entity;
using Cms.IRepository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;

namespace Cms.Service
{
public class BaseService<T>where T : class,new()
{
// 从子类的构造函数中传入
protected IBaseRepository<T> repository { get; set; }
public async Task<bool> DeleteEntityAsync(T entity)
{
return await repository.DeleteEntityAsync(entity);
}

public async Task<T> InsertEntityAsync(T entity)
{
return await repository.InsertEntityAsync(entity);
}

public IQueryable<T> LoadEntities(Expression<Func<T, bool>> wehreLambda)
{
return repository.LoadEntities(wehreLambda);
}

public IQueryable<T> LoadPageEntities<S>(int pageIndex, int pageSize, out int totalCount, Expression<Func<T, bool>> whereLambda, Expression<Func<T, S>> orderbyLambda, bool isAsc)
{
return repository.LoadPageEntities(pageIndex, pageSize, out totalCount, whereLambda, orderbyLambda, isAsc);
}

public async Task<bool> UpdateEntityAsync(T entity)
{
return await repository.UpdateEntityAsync(entity);
}
}
}

当然,在UserInfoService.cs这个类的构造函数中,要确定repository这个属性的值。

1
2
3
4
5
6
public class UserInfoService :BaseService<UserInfo>,IUserInfoService
{
public UserInfoService(IUserInfoRepository userInfoRepository) {
base.repository = userInfoRepository;
}
}

这里,我们可以看到在UserInfoService这个构造函数中,我们给父类BaseService中的repository传递的是userInfoRepository,表示使用的是用户的仓储,也就是对用户的数据进行操作。

1
2
3
4
5
6
7
8
public class UserInfoService :BaseService<UserInfo>,IUserInfoService
{
private readonly IUserInfoRepository _userInfoRepository;
public UserInfoService(IUserInfoRepository userInfoRepository) {
base.repository = userInfoRepository;
_userInfoRepository = userInfoRepository;
}
}

同时这里我们也定义了_userInfoRepository成员,同时在其构造方法中对其进行了注入,这里定义_userInfoRepository这个成员的目的是在当前的UserInfoService服务类中独有的方法中需要使用到对应的仓储的时候,使用的就是_userInfoRepository.

当目前为止服务层已经设计完毕。

6、配置依赖注入

6.1 基本注入实现

下面我们需要在应用表现层中,导入所有的项目(这里为了防止出现问题,导入所有项目),然后进行服务以及数据仓储的注入操作。

1
2
3
4
5
6
7
builder.Services.Configure<MvcOptions>(c =>
{
c.Filters.Add<UnitOfWorkFilter>();
});
// 注入
builder.Services.AddScoped<IUserInfoService, UserInfoService>();
builder.Services.AddScoped<IUserInfoRepository, UserInfoRepository>();

Program.cs文件中添加以上的注入代码。

下面进行测试。

创建UserInfoController.cs,然后添加如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class UserInfoController : Controller
{
private readonly IUserInfoService userInfoService;

public UserInfoController(IUserInfoService userInfoService)
{
this.userInfoService = userInfoService;
}

public IActionResult Index()
{

return View();
}
public IActionResult GetUser()
{
var userList = userInfoService.LoadEntities(u=>true);
return Json(new { data =userList } );
}
[UnitOfWork(new Type[] { typeof(MyDbContext) })]
public async Task<IActionResult> CreateUser() {

UserInfo userInfo = new UserInfo();
userInfo.UserName = "lisi";
userInfo.UserPhone = "12345678999";
userInfo.UserEmail = "lisi@126.com";
userInfo.UserPassword = "123";
userInfo.Gender = 1;
userInfo.CreateTime = DateTime.Now;
userInfo.UpdateTime = DateTime.Now;
userInfo.DelFlag = true;
userInfo.PhotoUrl = "b.jpg";
var user = await userInfoService.InsertEntityAsync(userInfo);
return Json(new { code = 200, msg = "添加成功",user });
}
}

在上面的代码中,在UserInfoController这个构造方法中,完成了IUserInfoService的注入操作。

同时创建了GetUser方法,来查询数据。

通过CreateUser方法来创建用户。注意这里需要执行SaveChanges方法来保存数据,所以添加了UnitOfWork这个Attribute.

启动项目进行测试。

6.2 通过Autofac来实现注入

是一个轻量级的依赖注入(DI)框架

在上一小节中,我们虽然已经实现了注入的操作,但是面临一个问题,就是每次添加一个服务或者是仓储,都需要手动的进行注入,操作起来比较的麻烦。

这里我们可以通过Autofac这个第三方的注入框架来完成注入的操作。

安装对应的包:

1
2
Install-Package Autofac
Install-Package Autofac.Extensions.DependencyInjection

这里我们先在Cms.WebUI这个应用项目中安装以上的包。注意:在【程序包管理器控制台】中选择Cms.WebUI

在这个应用项目中创建AutofaceDI文件夹,在该文件夹下面创建AutofacModuleRegister.cs类,该类中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using Autofac;
using System.Reflection;

namespace Cms.WebUI.AutofaceDI
{
public class AutofacModuleRegister:Autofac.Module
{
// 重写Autoface中的Load方法,从而实现注入的操作
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
var AppServices = Assembly.Load("Cms.Service");
var IAppServices = Assembly.Load("Cms.IService");
var AppRepository = Assembly.Load("Cms.Repository");
var IAppRepository = Assembly.Load("Cms.IRepository");
// 这里做了一个约定,服务层中的类都是以Service结尾,仓储项目中的类都是以Repository结尾
builder.RegisterAssemblyTypes(IAppServices, AppServices).Where(a => a.Name.EndsWith("Service")).AsImplementedInterfaces();//是以接口方式进行注入,

builder.RegisterAssemblyTypes(IAppRepository, AppRepository).Where(a => a.Name.EndsWith("Repository")).AsImplementedInterfaces();
}
}
}

在上面的代码中,我们重写了Load方法,这个方法中实现了注入的操作。

这里做了一个约定,服务层中的类都是以Service结尾,仓储项目中的类都是以Repository结尾.

修改Program.cs文件中的代码,如下所示:

1
2
3
4
5
6
7
/*builder.Services.AddScoped<IUserInfoService, UserInfoService>();
builder.Services.AddScoped<IUserInfoRepository, UserInfoRepository>();*/
// 替换容器,初始化一个Autofac的示例。
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()).ConfigureContainer<ContainerBuilder>(builder =>
{
builder.RegisterModule(new AutofacModuleRegister());
});

由于.net core内置的IOC容器是IServiceCollection,而这里我们需要使用Autofac来替换对应的容器,所以需要使用AutofacServiceProviderFactory这个工厂类。

当使用第三方容器的时候,同一通过ConfigureContainer这个方法来完成对应的配置,所以在该方法中,添加了所需要的服务。

7、AutoMapper搭建

现在有一个问题,我们要展示用户的信息,但是并不是展示全部,例如,用户的密码,用户的注册时间等信息不进行展示。

这样应该怎样进行处理呢?

我们可以创建一个UserInfoDTO.CS类,该类中定义的都是在前端需要展示的用户信息。

这里就需要通过AutoMapper这个库,把UserInfo.cs这个实体类,转换成UserInfoDTO.cs这个类。

cms.WebUI这个应用项目中安装对应的包

1
Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection

cms.WebUI项目中创建文件夹Profiles,在该文件夹下面创建CustomAutoMapperProfile.cs类,该类中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
namespace Cms.WebUI.Profiles
{
public class CustomAutoMapperProfile:Profile // 这里需要继承Profile类
{
public CustomAutoMapperProfile() {

CreateMap<UserInfo,UserInfoDTO>(); //在这里我们指定了映射关系
}
}
}

以上完成了映射的配置。

下面返回到Program.cs文件中,将CustomAutoMapperProfile添加到容器中。

1
2
3
4
// // 将AutoMapper添加到容器中
builder.Services.AddAutoMapper(typeof(CustomAutoMapperProfile));

var app = builder.Build();

下面在UserInfoController.cs控制器中,进行注入

1
2
3
4
5
6
7
8
9
10
11
public class UserInfoController : Controller
{
private readonly IUserInfoService userInfoService;
private readonly IMapper mapper; // 声明mapper

public UserInfoController(IUserInfoService userInfoService,IMapper mapper)
{
this.userInfoService = userInfoService;
this.mapper = mapper; // 完成注入
}

下面在GetUser方法中进行映射的操作

1
2
3
4
5
6
public IActionResult GetUser()
{
var userList = userInfoService.LoadEntities(u=>true);
var userDTOList = mapper.Map<List<UserInfoDTO>>(userList.ToList()); // 这里是完成了list集合的转换,这里如果转换IQueryable,会出现错误。
return Json(new { data = userDTOList } );
}

同时,我们还需要在cms.WebUI项目的Models目录下面创建UserInfoDTO.cs类,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
public class UserInfoDTO
{
public long Id { get; set; }
public string? UserName { get; set; }
public string? UserEmail { get; set; }
public string? UserPhone { get; set; }
public int Gender { get; set; }
/// <summary>
/// 用户头像,存储头像路径
/// </summary>
public string? PhotoUrl { get; set; }
}

我们可以看到这里不会展示用户的密码等信息。

启动项目,访问GetUser方法,查看效果。

通过浏览器中展示的结果,我们可以看到,最终展示的数据中没有用户密码等信息。

二、用户管理

1、展示用户基本信息

这里我们使用Miniui这个库来进行前端展示,它是基于jquery库来实现的。

后面我们做前后端分离项目的时候,我们使用的是Vue框架。

Miniui文档:http://www.miniui.com/docs/quickstart/

miniui这个文件夹拷贝到wwwroot目录下面,miniui这个文件夹下面包含了对应的样式与js文件。

针对UserInfoController控制器的index方法创建视图,该视图中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

@{
Layout = null; <!---注意:这里我们把MVC默认的模版去掉了-->
}
<html>
<head>
<script src="/lib/jquery/dist/jquery.min.js"></script>
<link href="/miniui/themes/default/miniui.css" type="text/css" rel="stylesheet" />

<script src="/js/miniui.js"></script>

</head>


<body>

<h1>Pagination 分页表格</h1>

<div style="padding-bottom:5px;">

<span>员工姓名:</span><input type="text" id="key" />
<input type="button" value="查找" onclick="search()" />

</div>

<div id="datagrid1" class="mini-datagrid" style="width:100%;height:250px;"
url="../data/AjaxService.aspx?method=SearchEmployees"
idField="id" allowResize="true"
sizeList="[20,30,50,100]" pageSize="20"
showHeader="true" title="表格面板"
onmouseup="return datagrid1_onmouseup()">
<div property="columns">
<div type="indexcolumn"></div>
<div field="loginname" width="120" headerAlign="center" allowSort="true">员工帐号</div>
<div field="name" width="120" headerAlign="center" allowSort="true">姓名</div>
<div field="gender" width="100" renderer="onGenderRenderer" align="center" headerAlign="center">性别</div>
<div field="salary" numberFormat="¥#,0.00" align="right" width="100" allowSort="true">薪资</div>
<div field="age" width="100" allowSort="true" decimalPlaces="2" dataType="float">年龄</div>
<div field="createtime" width="100" headerAlign="center" dateFormat="yyyy-MM-dd" allowSort="true">创建日期</div>
</div>
</div>

</body>

</html>

这个datagrid1就是miniui这个库提供的表格,具体的示例可以参考官网:

http://www.miniui.com/demo/#src=datagrid/pager.html

服务端返回的数据格式(具体的使用案例):

1
http://www.miniui.com/docs/tutorial/datagrid.html

下面修改视图中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<body>

<h1>用户信息管理</h1>

<div style="padding-bottom:5px;">

<span>员工姓名:</span><input type="text" id="key" />
<input type="button" value="查找" onclick="search()" />

</div>

<div id="datagrid1" class="mini-datagrid" style="width:60%;height:250px;" ajaxType="get" url="/userinfo/getuser"
idField="id" allowResize="true"
sizeList="[20,30,50,100]" pageSize="20"
showHeader="true" title="用户信息"
onmouseup="return datagrid1_onmouseup()">
<div property="columns">
<div type="indexcolumn"></div>
<!-----注意:这里修改了field属性,该属性的取值是服务端返回的数据-->
<div field="id" width="120" headerAlign="center" allowSort="true">员工编号</div><!--只保留了编号排序-->
<div field="userName" width="120" headerAlign="center" >姓名</div>
<div field="gender" width="100" renderer="onGenderRenderer" align="center" headerAlign="center">性别</div>
<div field="userEmail" align="right" width="100">用户邮箱</div>
<div field="userPhone" width="100" allowSort="true" >手机号码</div>
<div field="photoUrl" width="100" headerAlign="center">头像</div>
</div>
</div>
<script>
mini.parse(); // 获取mini对象
var grid = mini.get("datagrid1");
var url = "/userinfo/getuser";
grid.setUrl(url); // 设置服务端请求的地址
grid.load(); // 加载数据

</script>
</body>

启动项目,查看结果

1.1 处理性别

下面处理一下性别的展示

1
<div field="gender" width="100" renderer="onGenderRenderer" align="center" headerAlign="center">性别</div>

在这里添加了一个renderer属性,取值是onGenderRenderer,表示在渲染数据的时候,会调用onGenderRenderer函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
mini.parse();
var grid = mini.get("datagrid1");
var url = "/userinfo/getuser";
grid.setUrl(url);
grid.load();
// 这里定义了一个数组Genders数组,数组中的每一个元素都是一个对象,id取值是0表示是男
var Genders = [{ id:0, text: '男' }, { id: 1, text: '女' }];
function onGenderRenderer(e) {

// 参数e中存储的就是性别列传递过来的数据,我们知道,性别这一列中value属性存储的数据是0,或者是1
for (var i = 0, l = Genders.length; i < l; i++) {
var g = Genders[i];
if (g.id == e.value) return g.text;
}
return "";
}
</script>

1.2 头像处理

在项目中的wwwroot目录下面创建images文件夹,该文件夹中存储的就是用户的头像照片。

同时,这里需要把数据库中T_UserInfos表中的字段PhotoUrl字段的值修改一下,修改成/images/f.jpg

1
<div field="photoUrl" width="100" headerAlign="center" renderer="onPhotoRenderer">头像</div>

可以看到这里我们也是修改了头像列,为其添加了 renderer="onPhotoRenderer"

对应的onPhotoRenderer函数的具体实现如下所示:

1
2
3
4
// 展示头像
function onPhotoRenderer(e){
return "<img width ='100px' height ='100px' src ="+e.value + ">"
}

1.3 分页展示数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public IActionResult GetUser()
{
var pageIndex = string.IsNullOrEmpty(Request.Query["pageIndex"]) ? 1:int.Parse(Request.Query["pageIndex"]!)+1; // 注意:这里浏览器端传递过来的pageIndex的值第一页是0
var pageSize = string.IsNullOrEmpty(Request.Query["pageSize"]) ? 5 : int.Parse(Request.Query["pageSize"]!);

var sortOrder = string.IsNullOrEmpty(Request.Query["sortOrder"]) ? "asc" : Request.Query["sortOrder"].ToString();
var order = sortOrder == "asc" ? true : false;
/*
var userList = userInfoService.LoadEntities(u=>true);*/
int totalCount = 0;
var userList = userInfoService.LoadPageEntities<long>(pageIndex, pageSize, out totalCount, u => u.DelFlag == true, u => u.Id, order);
var userDTOList = mapper.Map<List<UserInfoDTO>>(userList.ToList());
return Json(new { total = totalCount, data = userDTOList } ); // 返回的数据格式,总的记录数必须赋值给total属性
}

同时,前端能够看到展示的效果,调整了sizeList,pageSize属性的值。

1
2
3
<div id="datagrid1" class="mini-datagrid" style="width:60%;height:250px;" ajaxType="get" url="/userinfo/getuser"         
idField="id" allowResize="true"
sizeList="[2,5,10,30]" pageSize="2"

2、用户信息搜索

在页面中增加搜索框,如下所示:

1
2
3
4
5
6
7
<div style="padding-bottom:5px;">

<span>员工姓名:</span><input type="text" id="name" />
<span>员工邮箱:</span><input type="text" id="email"/>
<input type="button" value="查找" onclick="search()" />

</div>

search方法的实现

1
2
3
4
5
6
// 用户信息搜索
function search(){
var searchName = document.getElementById("name").value;
var searchEmail = document.getElementById("email").value;
grid.load({name:searchName,email:searchEmail}); //发送请求,加载表格中的数据,这里可以参考文档
}

服务端实现:

1
2
3
var order = sortOrder == "asc" ? true : false;
var userName = Request.Query["name"];
var userEmail = Request.Query["email"];

这里首先接收传递过来的搜索条件。

然后作为LoadPageEntities方法的查询参数,但是这样导致了LoadPageEntities方法的参数比较多。

所以这里我们可以对参数进行封装处理。

Cms.Entity这个项目中,创建一个文件夹Search,在该文件夹下面创建userInfoSearch.cs类,该类中构建了搜索条件,代码如下所示:

1
2
3
4
5
6
7
8
9
namespace Cms.WebUI.Models.Search
{
public class userInfoSearch:BaseSearch
{
public string? UName { get; set; }
public string? UEmail { get; set; }
}
}

搜索的结果展示也需要分页展示,所以分页展示所需要的属性封装到了BaseSearch类中,这里的UserInfoSerach类会继承BaseSearch类。

BaseSerach.cs类中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
namespace Cms.WebUI.Models.Search
{
public class BaseSearch
{
public int PageIndex { get;set; }
public int PageSize { get; set; }
public int TotalCount { get; set; }
public bool Order { get; set; }
}
}

下面返回到UserInfoController.cs这个控制器中构建搜索的条件,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var userName = Request.Query["name"];
var userEmail = Request.Query["email"];
/*
var userList = userInfoService.LoadEntities(u=>true);*/
int totalCount = 0;
// 构建搜索条件
userInfoSearch userInfoSearch = new userInfoSearch()
{
UName = userName,
UEmail = userEmail,
PageIndex = pageIndex,
PageSize = pageSize,
TotalCount = totalCount,
Order = order
};

然后将userInfoSearch对象传递给服务层,但是在服务层中定义的方法,没有满足该条件的。

所以修改IUserInfoService.cs这个接口中的代码,如下所示:

1
2
3
4
public interface IUserInfoService:IBaseService<UserInfo>
{
IQueryable<UserInfo> LoadSearchEntities(userInfoSearch userInfoSearch, bool delFlag);
}

这里我们又定义了一个LoadSearchEntities方法。

下面要实现该方法,修改UserInfoService.cs这个业务类中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

private readonly IUserInfoRepository _userInfoRepository;
public UserInfoService(IUserInfoRepository userInfoRepository) {
base.repository = userInfoRepository;
_userInfoRepository = userInfoRepository;
}

// 搜索方法的实现
public IQueryable<UserInfo> LoadSearchEntities(userInfoSearch userInfoSearch, bool delFlag)
{
var temp = _userInfoRepository.LoadEntities(u => u.DelFlag == delFlag);
if (!string.IsNullOrEmpty(userInfoSearch.UName))
{
temp = temp.Where<UserInfo>(u=>u.UserName!.Contains(userInfoSearch.UName));
}
if (!string.IsNullOrEmpty(userInfoSearch.UEmail))
{
temp = temp.Where<UserInfo>(u=>u.UserEmail!.Contains(userInfoSearch.UEmail));
}
userInfoSearch.TotalCount = temp.Count();
if (userInfoSearch.Order)
{
return temp.OrderBy<UserInfo,long>(u => u.Id).Skip<UserInfo>((userInfoSearch.PageIndex-1)*userInfoSearch.PageSize).Take<UserInfo>(userInfoSearch.PageSize);
}
else
{
return temp.OrderByDescending<UserInfo, long>(u => u.Id).Skip<UserInfo>((userInfoSearch.PageIndex - 1) * userInfoSearch.PageSize).Take<UserInfo>(userInfoSearch.PageSize);
}

return temp;
}

下面再次返回到UserInfoController.cs这个控制器中进行代码的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int totalCount = 0;
// 构建搜索条件
userInfoSearch userInfoSearch = new userInfoSearch()
{
UName = userName,
UEmail = userEmail,
PageIndex = pageIndex,
PageSize = pageSize,
TotalCount = totalCount,
Order = order
};
/* var userList = userInfoService.LoadPageEntities<long>(pageIndex, pageSize, out totalCount, u => u.DelFlag == true, u => u.Id, order);*/
// 这里调用了LoadSearchEntities方法进行搜索,传递的是对象userInfoSearch,true表示展示的是没有被逻辑删除的用户信息
var userList = userInfoService.LoadSearchEntities(userInfoSearch,true);
var userDTOList = mapper.Map<List<UserInfoDTO>>(userList.ToList());
return Json(new { total = userInfoSearch.TotalCount, data = userDTOList } );// 注意:这里返回的总的记录数是userInfoSearch.TotalCount属性中的值

注意:这里返回的总的记录数是userInfoSearch.TotalCount属性中的值

启动项目进行测试。

当然,这里还有一个小问题,就是当调用LoadSearchEntities方法的时候,传递的true表示的是查询没有被逻辑删除的用户信息,但是这里写成true,含义不明确,最好定义成枚举类型。

Cms.Entity类库项目中创建一个Enum文件夹,在该文件夹中创建一个DelFlagEnum.cs文件,该文件中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

namespace Cms.Entity.Enum
{
public enum DelFlagEnum
{
/// <summary>
/// 逻辑删除
/// </summary>
logicDelete = 0,
/// <summary>
/// 正常
/// </summary>
Normal = 1
}
}

返回到UserInfoController.cs控制器中,进行代码的修改

1
2
bool delFlag =Convert.ToBoolean(DelFlagEnum.Normal);
var userList = userInfoService.LoadSearchEntities(userInfoSearch, delFlag);

这里获取枚举中的DelFlagEnum.Normal,并且将其转换成bool类型。

然后作为了LoadSearchEntities方法的参数。

3、删除用户信息

参考文档:

1
http://www.miniui.com/demo/#src=datagrid/rowedit.html

前端视图页面处理

1
2
3
<div field="photoUrl" width="100" headerAlign="center" renderer="onPhotoRenderer">头像</div>
<!--这里添加了操作列-->
<div name="action" width="120" headerAlign="center" align="center" renderer="onActionRenderer" cellStyle="padding:0;">操作</div>

onActionRenderer函数的实现如下所示:

1
2
3
4
5
6
7
8
9
10
// “操作”列实现
function onActionRenderer(e) {

var record = e.record;
var uid = record.id;
var s = '<a class="Edit_Button" href="javascript:editRow(\'' + uid + '\')" >编辑</a>'
+ ' <a class="Delete_Button" href="javascript:delRow(\'' + uid + '\')">删除</a>';

return s;
}

以上代码参考文档中的案例。

delRow函数的实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 删除方法
function delRow(userId){
if(confirm("确定要删除该记录吗?")){
grid.loading("删除中,请稍后......");
$.ajax({
url:'/userInfo/deleteUser?userId='+userId,
success:function(data){
if(data.code ==200){
grid.reload();
alert(data.msg);
}
},
error:function(data){
alert(data.msg)
}

});
}
}

这里请求了/userInfo/deleteUser,同时传递了userId这个用户的编号。

DeleteUser方法的实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#region 删除用户信息
[UnitOfWork(new Type[] { typeof(MyDbContext) })] //注意:这里需要添加UnitOfWorkAttrbuite
public async Task<IActionResult> DeleteUser()
{
var userId =Convert.ToInt32(Request.Query["userId"]);
bool delFlag = Convert.ToBoolean(DelFlagEnum.logicDelete); // 逻辑删除
var userInfo = await userInfoService.LoadEntities(u=>u.Id == userId).FirstOrDefaultAsync();
if (userInfo != null)
{
userInfo.DelFlag = delFlag;
await userInfoService.UpdateEntityAsync(userInfo);
return Json(new { code = 200,msg ="删除成功" });
}
else
{
return Json(new { code = 501, msg = "删除失败" });
}
}
#endregion

4、批量删除用户

在上一小节中,我们虽然实现了删除操作,但是每次只能删除一条记录,在这一小节中,我们来看一下怎样批量删除多条记录。

前端页面的处理。

参考文档:http://www.miniui.com/demo/#src=datagrid/datagrid.html

首先增加一个复选框列

1
2
3
4
5
6
7
8
9
10
<div id="datagrid1" class="mini-datagrid" style="width:60%;height:550px;" ajaxType="get" url="/userinfo/getuser"         
idField="id" allowResize="true"
sizeList="[2,5,10,30]" pageSize="2"
showHeader="true" title="用户信息"
multiSelect="true" allowCellEdit="true" allowCellSelect="true" <!--注意这里必须添加这三个属性否则复选框无法选中-->
onmouseup="return datagrid1_onmouseup()">
<div property="columns">
<div type="indexcolumn" ></div>
<div type="checkcolumn"></div><!--这里增加了一个复选框列-->
<div field="id" width="120" headerAlign="center">编号</div>

同时在datagrid这个表格上面增加“删除按钮”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--这里添加  增加按钮与删除按钮 开始-->
<div style="width:800px;">
<div class="mini-toolbar" style="border-bottom:0;padding:0px;">
<table style="width:100%;">
<tr>
<td style="width:100%;">
<a class="mini-button" iconCls="icon-add" onclick="add()">增加</a>
<a class="mini-button" iconCls="icon-remove" onclick="remove()">删除</a>
</td>
</tr>
</table>
</div>
</div>
<!--这里添加 增加按钮与删除按钮 结束-->
<div id="datagrid1" class="mini-datagrid" style="width:60%;height:550px;" ajaxType="get" url="/userinfo/getuser"

同时,按钮上面有图标,所以这里还需要引入对应的样式

1
2
3
4
5
<script src="/lib/jquery/dist/jquery.min.js"></script>
<link href="/miniui/themes/default/miniui.css" type="text/css" rel="stylesheet" />
<link href="/miniui/themes/icons.css" type="text/css" rel="stylesheet" /> <!--这里添加了图标样式-->
<link href="/miniui/themes/bootstrap/skin.css" type="text/css" rel="stylesheet" /> <!--这里给表格换成了bootstrap的样式--->
<script src="/js/miniui.js"></script>

当单击“删除”按钮的时候,要调用remove这个函数,该函数的实现方式如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 批量删除
function remove(){
var rows = grid.getSelecteds();
if(rows.length > 0){
if (confirm("确定删除选中记录?")) {
var ids = [];
for (var i = 0, l = rows.length; i < l; i++) {
var r = rows[i];
ids.push(r.id);
}
var id = ids.join(',');
grid.loading("操作中,请稍后......");
$.ajax({
url: "/userInfo/deleteUsers?userIds=" + id,
success: function (data) {
if(data.code === 200){
grid.reload();
alert(data.msg);
}

},
error: function () {

}
});
}
}else{
alert("请选中一条记录");
}
}

下面实现UserInfo控制器中的DeleteUsers方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#region 批量删除用户
[UnitOfWork(new Type[] { typeof(MyDbContext) })] // 注意:这里使用了UnitOfWork这个Attribute
public async Task<IActionResult> DeleteUsers()
{
string strId = Request.Query["userIds"]!;
string[]strIds = strId.Split(',');
List<long> list = new List<long>(); // 注意:这里指定的类型是long
foreach(string id in strIds)
{
list.Add(Convert.ToInt32(id));
}
await userInfoService.DeleteEntities(list); // 这里需要进行批量删除,需要再服务层中定义该方法。
return Json(new { code = 200, msg = "删除成功" });
}
#endregion

说明:以上控制器中的方法的代码业务是否太重,这里,我们完全可以把strIds传递到DeleteEntities这业务方法中,在该方法中执行循环操作,尽量让控制器中的方法不包含业务的内容。

修改IUserInfoService.cs这个接口中的代码,如下所示:

1
2
3
4
5
6
public interface IUserInfoService:IBaseService<UserInfo>
{
IQueryable<UserInfo> LoadSearchEntities(userInfoSearch userInfoSearch, bool delFlag);
// 这里声明了DeleteEntities方法
Task<bool> DeleteEntities(List<long> list);
}

下面修改UserInfoService.cs这个类中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// 批量删除
/// </summary>
/// <param name="list"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<bool> DeleteEntities(List<long> list)
{
var userInfoList = _userInfoRepository.LoadEntities(u => list.Contains(u.Id));
foreach (var userInfo in userInfoList)
{
userInfo.DelFlag =Convert.ToBoolean(DelFlagEnum.logicDelete); //进行逻辑删除
await _userInfoRepository.UpdateEntityAsync(userInfo); // 这里执行了更新的操作。
}
return await Task.FromResult(true);
}

当然,这里会生成多条selectupdate语句,可以使用我们前面讲解的批量删除的方式。

启动项目进行测试。

5、新增用户1

前端视图中的代码修改(这里修改UserInfoController.cs控制器中的index方法对应的index视图)

index这个视图中,首先添加一个新增按钮

1
2
3
4
<td style="width:100%;">
<a class="mini-button" iconCls="icon-add" onclick="add()">增加</a>
<a class="mini-button" iconCls="icon-remove" onclick="remove()">删除</a>
</td>

当单击增加按钮的时候,会调用add这个函数,该函数的实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 新增用户信息
// http://www.miniui.com/docs/api/index.html#ui=messagebox
// 以上是弹出窗口文档
function add(){
mini.open({
targetWindow: window,
url: "/userInfo/showAdd",
title: "新增员工", width: 600, height: 400,
onload: function () {
var iframe = this.getIFrameEl();
iframe.contentWindow;// 文档中这里调用了SetData方法,指的是在打开的页面中会有setData函数,而我们这里showAdd方法对应的视图中没有定义SetData函数,
//所以这里不用调用
},
ondestroy: function (action) {

grid.reload();
}
});
}

在以上的代码中,会弹出一个窗口,在该窗口中展示的就是showAdd这个视图页面。

关于该页面中的内容就是添加用户信息的表单。

关于该表单的设计,可以参考如下文档中的内容:

1
http://www.miniui.com/demo/#src=datagrid/datagrid.html

在该文档中,有如下案例

弹出窗口所展示的表单:参考文档:

通过上图可以看到,这里弹出的窗口中展示的是EmployeeWindow.html 页面。该页面中也是一个表单页面,具体访问地址,如下所示:

1
2
3
http://www.miniui.com/demo/#src=datagrid/datagrid.html
具体的表单示例代码,访问EmployeeWindow.html 页面,该页面的地址:www.miniui.com/demo/CommonLibs/EmployeeWindow.html
打开该页面以后,查看对应的HTML代码

下面我们需要在UserInfoController.cs这个控制器中创建ShowAdd这个方法,该方法的作用就是为了呈现ShowAdd视图,代码如下所示:

1
2
3
4
public IActionResult ShowAdd()
{
return View();
}

ShowAdd方法对应的视图中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@{
Layout = null;
}
<html>
<head>
<script src="/lib/jquery/dist/jquery.min.js"></script>
<link href="/miniui/themes/default/miniui.css" type="text/css" rel="stylesheet" />
<link href="/miniui/themes/icons.css" type="text/css" rel="stylesheet" />
<link href="/miniui/themes/bootstrap/skin.css" type="text/css" rel="stylesheet" />
<script src="/js/miniui.js"></script>
<script src="/js/form-serialize.js"></script> <!--这里需要引入form-serialize.js文件-->
</head>

<body>

<form id="form1">
<mini-hidden name="id"></mini-hidden>
<div style="padding-left:11px;padding-bottom:5px;">
<table style="table-layout:fixed;">
<tr>
<td style="width:90px;">用户姓名:</td>
<td style="width:150px;">
<input type ="text" name="UserName" placeholder="请输入姓名" autocomplete="off" />
</td>
<td style="width:90px;">用户密码</td>
<td>
<input type="password" name="UserPassword" placeholder="请输入密码"/>
</td>
</tr>
<tr>
<td style="width:90px;">用户邮箱:</td>
<td>
<input type="text" name="UserEmail" placeholder="请输入邮箱" autocomplete ="off"/>
</td>
<td style="width:90px;">手机号码:</td>
<td>
<input type="text" name="UserPhone" placeholder="请输入手机号码" autocomplete ="off"></input>
</td>
</tr>

<tr>
<td style="width:90px;">性别:</td>
<td>
<select name="gender" class="mini-radiobuttonlist">
<option value="0"></option>
<option value="1"></option>
</select>
</td>
<td>头像:</td>
<td>
<input class="mini-htmlfile" name="PhotoUrl" type="file" />
</td>
</tr>
</table>
</div>
<div style="text-align:center;padding:10px;">
<mini-button onclick="onOk" style="width:60px;margin-right:20px;">确定</mini-button>
<mini-button onclick="onCancel" style="width:60px;">取消</mini-button>
</div>
</form>
</body>
</html>
<script>
var form = document.getElementById("form1");
// 关闭窗口
function onCancel() {
window.CloseOwnerWindow();
}
// 保存表单中的数据
function onOk() {
SaveData();
}
function SaveData() {
let data = serialize(form);
console.log("data = ",data);
$.ajax({
url: '/userInfo/CreateUser',
method:"post",
data:data,
success:function(data){
},
error:function(error){

}
})

}

</script>

当单击取消按钮的时候会调用onCancel方法,该方法就是调用了window.CloseOwnerWindow中的方法关闭了窗口。

当单击确定按钮的时候,会调用SaveData方法,通过serialize方法获取表单中的数据,然后发送ajax请求。

这里有一个问题,就是用户头像的问题。这里需要先上传用户的头像,然后再把头像的地址发送到服务端,最终保存到数据库中。

6、用户头像上传

下面修改ShowAdd.cshtml这个视图文件中的代码

首先引入样式与axios.js这个文件

1
2
3
4
<link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css"> <!--这里引入了bootstrap.css-->
<script src="/js/miniui.js"></script>
<script src="/js/form-serialize.js"></script>
<script src ="/js/axios.js" ></script> <!--这里引入了axios.js-->

下面在添加样式,控制上传成功的头像的展示效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script src ="/js/axios.js" ></script>
<!--下面添加了样式-->
<style>
.thumb-box {
text-align: center;
margin-top: 50px;
}

.thumb {
width: 250px;
height: 250px;
object-fit: cover;
border-radius: 50%;
}
</style>

下面给文件上传域添加对应的属性

1
2
3
4
<td>头像:</td>
<td>
<input class="mini-htmlfile" type="file" id="iptFile" accept="image/*" />
</td>

这里添加了一个id属性,accept属性。

1
2
3
4
5
6
7
8
9
10
11
<tr>
<td style="width:190px;">
<button class="btn btn-primary" id="btnChoose">选择 & 上传图片</button>
</td>
<td>
<div class="thumb-box">
<!-- 头像 -->
<img src="" class="img-thumbnail thumb" alt="">
</div>
</td>
</tr>

同时在表格中又添加了两行,一个展示上传图片的按钮,另一行中展示了上传成功的头像。

下面实现对应的js代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
var form = document.getElementById("form1");
let btnChoose = document.querySelector('#btnChoose') // -----------获取上传按钮
let iptFile = document.querySelector('#iptFile') // -------------找到文件上传域
let img = document.querySelector('.thumb') // -------------找到展示头像图片的img标签
//-----------给上传按钮注册单击事件
btnChoose.addEventListener('click', function (e) {
e.preventDefault(); //----------------注意这里一定要阻止默认行为,因为按钮在表单中会有提交的动作。
iptFile.click()
})
// FormData 存文件 ==> axios 发请求 (看接口文档)
iptFile.addEventListener('change', function () {
// 用户选择的文件,该如何拿到?
// console.log(this.files[0])

// 当this.files[0] 是undefined的时候,表示用户未选中文件,以下代码不用执行
if (!this.files[0]) {
return // 阻止后续代码的执行
}

// FormData 构造函数 new一起使用
let fd = new FormData()
fd.append('avatar', this.files[0])

// axios 发请求代码
axios({
method: 'post',
// 请求的url地址一定要从接口文档中去复制
url: '/userInfo/fileupload',
// data的值是fd(把FormData存的数据给发送到服务器上)
data: fd
}).then(({ data: res }) => {
// console.log(res)

// 更换图片(以下写法有问题,res.url 缺少根路径)
// img.src = res.url // error
let photourl = document.getElementById("photourl");
photourl.value = res.url; // ------------------- 注意:将上传成功的头像的路径保存到隐藏域中
img.src = `http://localhost:5219/${res.url}`
})
})

问题:当上传成功以后,我们会将服务端返回的头像图片的路径存储到一个隐藏域中,原因就是:当单击确认按钮的时候,会将图片路径发送到服务端,然后和其它用户的信息一起保存到数据库中。

所以在form表单中,我们要添加一个隐藏域,如下所示:

1
2
3
<form id="form1">
@* <mini-hidden name="id"></mini-hidden>*@
<input type="hidden" name="PhotoUrl" id="photourl"/>

同样这里还需要注意必须添加name属性,该属性的取值与实体类UserInfo.cs中的PhotoUrl属性保持同名,方便后面提交表单。

下面需要实现的就是UserInfoController.cs中的FileUpload方法,该方法的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public async Task<IActionResult> FileUpload()
{
var file = Request.Form.Files["avatar"]; // 接收上传的图片文件
if (file == null) return Json(new { code = 500, msg = "请选择上传文件" });
string fileName = Guid.NewGuid().ToString() + Path.GetExtension(file.FileName);
// 构建文件存储的路径(一定要放在wwwroot目录下面)
var filePath = Path.Combine(webHostEnvironment.ContentRootPath, "wwwroot/images", fileName);

using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
await file.CopyToAsync(fileStream);
return Json(new { code = 200, url = "images/" + fileName }); // 将上传成功的图片路径进行返回
}
}

同样这里需要注入webHostEnvironment

1
2
3
4
5
6
7
8
9
10
11
 private readonly IUserInfoService userInfoService;
private readonly IMapper mapper;
private readonly IWebHostEnvironment webHostEnvironment;
// ------------这里注入IWebHostEnvironment
public UserInfoController(IUserInfoService userInfoService,IMapper mapper, IWebHostEnvironment webHostEnvironment)
{
this.userInfoService = userInfoService;
this.mapper = mapper;
this.webHostEnvironment = webHostEnvironment;
}

启动项目进行测试。

7、完成新增用户操作

在这一小节中,我们要完成用户信息的保存工作。

完善UserInfoController.cs控制器中的CreateUser方法中的代码

1
2
3
4
5
6
7
8
9
10
11
#endregion
[UnitOfWork(new Type[] { typeof(MyDbContext) })]
public async Task<IActionResult> CreateUser(UserInfo userInfo) {
userInfo.CreateTime = DateTime.Now;
userInfo.UpdateTime = DateTime.Now;
userInfo.DelFlag = Convert.ToBoolean(DelFlagEnum.Normal);
// userInfo.PhotoUrl = userInfo.PhotoUrl;
var user = await userInfoService.InsertEntityAsync(userInfo);
return Json(new { code = 200, msg = "添加成功", user });

}

问题:当单击了弹出窗口的确定按钮以后,第一个问题就是需要关闭窗口。

第二个问题,我们发现,在表格中无法正常的展示图片。

首先解决第二个问题:

我们可以审查元素,发现图片的路径如下所示:

1
<img width="100px" height="100px" src="images/c6fa233f-d144-4923-9156-5a7e121e84e6.jpg">

这里很明显是相对路径,我们应该修改成绝对路径

修改UserInfoController.cs控制器中的FileUpload方法中的代码

1
return Json(new { code = 200, url = "/images/" + fileName }); // 将上传成功的图片路径进行返回

这里直接返回绝对路径的形式

下面修改ShowAdd.cshtml视图中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function SaveData() {
let data = serialize(form);
console.log("data = ",data);
$.ajax({
url: '/userInfo/CreateUser',
method:"post",
data:data,
success:function(data){
onCancel(); // 当添加成功以后,直接调用onCancel方法关闭窗口,当然这里也可以做一个判断,给出用户添加成功的提示信息。
},
error:function(error){

}
})

}

8、编辑用户信息

8.1 展示编辑的信息

修改index.cshtml这个视图中的代码

我们知道当单击编辑链接的时候,会执行editRow这个函数,该函数对应的实现代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 编辑用户信息
function editRow(userId) {
mini.open({
targetWindow: window,
url: "/userInfo/showEdit?userId="+userId, // 传递了用户的编号
title: "编辑员工", width: 700, height: 600,
onload: function () {
var iframe = this.getIFrameEl();
iframe.contentWindow;
},
ondestroy: function (action) {

grid.reload();
}
});
}

下面在UserInfoController.cs 控制器中添加showEdit这个方法,该方法中的代码实现如下所示:

1
2
3
4
5
6
7
8
#region 展示编辑用户信息窗口
public async Task<IActionResult> ShowEdit()
{
var userId =Convert.ToInt32(Request.Query["userId"]);
var userInfo = await userInfoService.LoadEntities(u=>u.Id == userId).FirstOrDefaultAsync();
return View(userInfo);
}
#endregion

接收传递过来的用户编号,然后根据用户的编号查询用户的信息,然后传递给视图中进行展示。

ShowEdit.cshtml视图中代码,该视图中还是使用了添加用户时的表单,这里为什么没有使用同一个页面来完成添加与更新操作呢?

主要考虑的就是公用一个页面,会导致页面逻辑变得复杂,所以建议更新与添加分开到两个页面中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
@using Cms.Entity; //---------------------------------这里导入了命名空间
@model UserInfo; //-----------------------这里指定了页面所关联的类型
@{
Layout = null;
}
<html>
<head>
<script src="/lib/jquery/dist/jquery.min.js"></script>
<link href="/miniui/themes/default/miniui.css" type="text/css" rel="stylesheet" />
<link href="/miniui/themes/icons.css" type="text/css" rel="stylesheet" />
<link href="/miniui/themes/bootstrap/skin.css" type="text/css" rel="stylesheet" />
<link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css">
<script src="/js/miniui.js"></script>
<script src="/js/form-serialize.js"></script>
<script src="/js/axios.js"></script>
<style>
.thumb-box {
text-align: center;
margin-top: 50px;
}

.thumb {
width: 250px;
height: 250px;
object-fit: cover;
border-radius: 50%;
}
</style>
</head>

<body>

<form id="form1">
@* <mini-hidden name="id"></mini-hidden>*@
//-----------------这里注意不要忘记将头像图片的地址赋值给隐藏域,因为用户有可能不会修改头像,这样就会导致该隐藏域中没有值。
<input type="hidden" name="PhotoUrl" id="photourl" value="@Model.PhotoUrl"/>
<div style="padding-left:11px;padding-bottom:5px;">
<table style="table-layout:fixed;">
<tr>
<td style="width:90px;">用户姓名:</td>
<td style="width:150px;">
<input type="text" name="UserName" placeholder="请输入姓名" autocomplete="off" value="@Model.UserName" /> // ------------给表单元素填充值
</td>
<td style="width:90px;">用户密码</td>
<td>
<input type="password" name="UserPassword" placeholder="请输入密码" value="@Model.UserPassword" />//--------------------给表单元素填充值
</td>
</tr>
<tr>
<td style="width:90px;">用户邮箱:</td>
<td>
<input type="text" name="UserEmail" placeholder="请输入邮箱" autocomplete="off" value="@Model.UserEmail" /> // -------------------给表单元素填充值
</td>
<td style="width:90px;">手机号码:</td>
<td>
<input type="text" name="UserPhone" placeholder="请输入手机号码" autocomplete="off" value="@Model.UserPhone"></input> // -------------------给表单元素填充值
</td>
</tr>

<tr>
<td style="width:90px;">性别:</td>
<td>
// ------------------------判断性别的范围
<select name="gender" class="mini-radiobuttonlist">
@{
if(Model.Gender == 0)
{
<option value="0" selected>男</option>
<option value="1">女</option>
}
else
{
<option value="0" >男</option>
<option value="1" selected>女</option>
}
}

</select>
</td>
<td>头像:</td>
<td>
<input class="mini-htmlfile" type="file" id="iptFile" accept="image/*" />
</td>

</tr>
<tr>
<td style="width:190px;">
<button class="btn btn-primary" id="btnChoose">选择 & 上传图片</button>
</td>
<td>
// -----------------给表单元素填充值,这里是将用户的头像地址赋值给了img标签的src属性
<div class="thumb-box">
<!-- 头像 -->
<img src="@Model.PhotoUrl" class="img-thumbnail thumb" alt="">
</div>
</td>
</tr>
</table>
</div>
<div style="text-align:center;padding:10px;">
<mini-button onclick="onOk" style="width:60px;margin-right:20px;">确定</mini-button>
<mini-button onclick="onCancel" style="width:60px;">取消</mini-button>
</div>
</form>
</body>
</html>
<script>
var form = document.getElementById("form1");
let btnChoose = document.querySelector('#btnChoose')
let iptFile = document.querySelector('#iptFile')
let img = document.querySelector('.thumb')
btnChoose.addEventListener('click', function (e) {
e.preventDefault();
iptFile.click()
})
// FormData 存文件 ==> axios 发请求 (看接口文档)
iptFile.addEventListener('change', function () {
// 用户选择的文件,该如何拿到?
// console.log(this.files[0])

// 当this.files[0] 是undefined的时候,表示用户未选中文件,以下代码不用执行
if (!this.files[0]) {
return // 阻止后续代码的执行
}

// FormData 构造函数 new一起使用
let fd = new FormData()
fd.append('avatar', this.files[0])

// axios 发请求代码
axios({
method: 'post',
// 请求的url地址一定要从接口文档中去复制
url: '/userInfo/fileupload',
// data的值是fd(把FormData存的数据给发送到服务器上)
data: fd
}).then(({ data: res }) => {
// console.log(res)

// 更换图片(以下写法有问题,res.url 缺少根路径)
// img.src = res.url // error
let photourl = document.getElementById("photourl");
photourl.value = res.url;
img.src = `http://localhost:5219/${res.url}`
})
})

// 关闭窗口
function onCancel() {
window.CloseOwnerWindow();
}
// 保存表单中的数据
function onOk() {
SaveData();
}
function SaveData() {
let data = serialize(form);
$.ajax({
url: '/userInfo/EditUser', //-----------------------------这里修改了地址
method: "post",
data: data,
success: function (data) {
onCancel();
},
error: function (error) {

}
})

}

</script>

8.2 完成用户信息编辑

下面在UserInfoController.cs控制器中创建EditUser这个方法,该方法实现的代码如下所示:

1
2
3
4
5
6
7
8
9
[UnitOfWork(new Type[] { typeof(MyDbContext) })] // 注意添加UnitofWork这个Attribute
public async Task<IActionResult> EditUser(UserInfo userInfo)
{
userInfo.DelFlag = Convert.ToBoolean(DelFlagEnum.Normal); // 删除标记
userInfo.UpdateTime = DateTime.Now; // 更新时间。
await userInfoService.UpdateEntityAsync(userInfo);
return Json(new { code = 200, msg = "更新成功" });

}

但是,这里还有一个问题,更新的时候我们需要用户的Id编号。

当展示ShowEdit.cshtml这个视图的时候,已经传递了用户的编号,所以这里也需要将其保存到隐藏域中,同理,添加时间也是一样。

1
2
3
4
5
6
7
<form id="form1">
@* <mini-hidden name="id"></mini-hidden>*@
<input type="hidden" name="PhotoUrl" id="photourl" value="@Model.PhotoUrl"/>
// 隐藏域中保存了用户编号
<input type="hidden" name="Id" value="@Model.Id"/>
// 隐藏域中保存了添加时间
<input type="hidden" name="CreateTime" value="@Model.CreateTime" />

启动项目进行测试。

9、首页布局

参考官方文档:

1
http://www.miniui.com/docs/tutorial/layout.html

这里需要修改HomeController.cs控制器中的代码,这里只保留Index这个方法。

同时把Views目录下的Home目录删除掉,这里重新针对Index方法,创建相应的视图页面。

对应的Index.cshtml视图中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
@{
Layout = null;
}
<html>
<head>
<script src="/lib/jquery/dist/jquery.min.js"></script>
<link href="/miniui/themes/default/miniui.css" type="text/css" rel="stylesheet" />
<link href="/miniui/themes/icons.css" type="text/css" rel="stylesheet" />
<link href="/miniui/themes/bootstrap/skin.css" type="text/css" rel="stylesheet" />
<script src="/js/miniui.js"></script>
<style type="text/css">
.txt {
font-family: "微软雅黑", "Helvetica Neue",​Helvetica,​Arial,​sans-serif;
font-size: 28px;
font-weight: bold;
cursor: default;
margin-top:20px;
color: #444;
}

.topNav {
position: absolute;
right: 8px;
top: 12px;
font-size: 12px;
line-height: 25px;
}

.topNav a {
text-decoration: none;
font-weight: normal;
font-size: 12px;
line-height: 25px;
margin-left: 3px;
margin-right: 3px;
color: #333;
}

.topNav a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<!---撑满页面------------->
<div class="mini-fit">
<!------------------mini-fit内部的子元素宽高要100%--------------->
<div id="layout1" class="mini-layout " style="width:100%;height:100%;" borderStyle="border:solid 1px #aaa;">
<div region="north" height="80">
<div class="txt">CMS文章管理系统</div>
<div class="topNav">
<a href="#">Admin</a>
<a href="#">退出</a>
</div>
</div>
<div region="south" showSplit="false" showHeader="true" height="80">
south
</div>
<div region="west" width="200">
<!--------这里使用了tree控件----------->
<ul id="tree1" class="mini-tree" style="width:100px;padding:5px;height:100px"
showTreeIcon="true" textField="text" idField="id" value="base" expandOnNodeClick="true">
<li>
<!------------树中的每一个菜单项,添加了class属性与url属性,同时注册了onclick事件--------------->
<a href="javascript:void(0)" url="/userInfo/index" class="detailLink" onclick="getUrl(this)">用户管理</a>

</li>
<li>
<a href="javascript:void(0)" url="/articelInfo/index" class="detailLink" onclick="getUrl(this)">文章管理</a>

</li>
</ul>
</div>
<!--------------------在中间件区域--通过iframe来指定要展示的页面内容---------------------------------->
<div region="center" >
<iframe src="/userInfo" style="width:100%;height:100%" frameborder="0" id="frame"></iframe>
</div>
</div>
</div>

</body>

</html>
<script>
// 当单击了不同的菜单项以后,对应的iframe中展示不同的内容
function getUrl(e){
var url = e.getAttribute("url");
var frame = document.getElementById("frame");
frame.src = url;
}


</script>

启动项目进行测试。

三、文章管理

1、模型设计

Cms.Entity这个类库项目中创建ArticelInfo.cs类,该类中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class ArticelInfo:BaseEntity<long>
{
/// <summary>
/// 关 键 字
/// </summary>
public string? KeyWords { get; set; }
/// <summary>
/// 标题类型,例如:公告,图文,推荐等类型
/// </summary>
public string? TitleType { get; set; }
/// <summary>
/// 文章简短标题
/// </summary>
public string? Title { get; set; }
/// <summary>
/// 完整标题
/// </summary>
public string? FullTitle { get; set; }
/// <summary>
/// 文章导读
/// </summary>
public string? Intro { get; set; }
/// <summary>
/// 文章标题颜色
/// </summary>
public string? TitleFontColor { get; set; }
/// <summary>
/// 文章标题字体类型
/// </summary>
public string? TitleFontType { get; set; }
/// <summary>
/// 文章内容
/// </summary>
public string? ArticleContent { get; set; }
/// <summary>
/// 文章的作者
/// </summary>
public string? Author { get; set; }
/// <summary>
/// 文章来源
/// </summary>
public string? Origin { get; set; }

/// <summary>
/// 图片的地址
/// </summary>
public string? PhotoUrl { get; set; }
/// <summary>
/// 文章属于哪个类别
/// </summary>
public ArticelClass? ArticelClass { get; set;}
}

同时创建一个实体类ArticelClass.cs,代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12

public class ArticelClass:BaseEntity<int>
{
public string? ArticleClassName { get; set; }
public int ParentId { get; set; }

public string? Remark { get; set; }
/// <summary>
/// 类别下面很多文章
/// </summary>
public List<ArticelInfo> ArticelInfos { get; set; } = new List<ArticelInfo>();
}

定义配置类ArticelInfoConfig.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ArticelInfoConfig : IEntityTypeConfiguration<ArticelInfo>
{
public void Configure(EntityTypeBuilder<ArticelInfo> builder)
{
builder.ToTable("T_ArticelInfos");
builder.Property(x => x.KeyWords).HasMaxLength(500).IsRequired();
builder.Property(x => x.TitleType).HasMaxLength(200).IsRequired();
builder.Property(x => x.Title).HasMaxLength(100).IsRequired();
builder.Property(x => x.FullTitle).HasMaxLength(200).IsRequired();
builder.Property(x => x.Intro).HasMaxLength(500).IsRequired();
builder.Property(x => x.TitleFontColor).HasMaxLength(50).IsRequired();
builder.Property(x => x.TitleFontType).HasMaxLength(50).IsRequired();
builder.Property(x => x.ArticleContent).IsRequired();
builder.Property(x => x.Author).HasMaxLength(20).IsRequired();
builder.Property(x => x.Origin).HasMaxLength(100).IsRequired();
builder.Property(x => x.PhotoUrl).HasMaxLength(500).IsRequired();
// 一个类别下面多篇文章
builder.HasOne<ArticelClass>(c => c.ArticelClass).WithMany(a => a.ArticelInfos).IsRequired();

}
}

定义配置类ArticelClassConfig.cs

1
2
3
4
5
6
7
8
9
10
public class ArticelClassConfig : IEntityTypeConfiguration<ArticelClass>
{
public void Configure(EntityTypeBuilder<ArticelClass> builder)
{
builder.ToTable("T_ArticelClasses");
builder.Property(x => x.ArticleClassName).HasMaxLength(50).IsRequired();
builder.Property(x => x.Remark).HasMaxLength(200).IsRequired();

}
}

注意:一定不要忘记创建DbSet对象

1
2
3
4
5
public class MyDbContext:DbContext
{
public DbSet<UserInfo>UserInfos { get; set; }
public DbSet<ArticelClass>ArticelClasses { get; set; }
public DbSet<ArticelInfo>ArticelInfos { get; set; }

下面进行数据库的迁移操作

在【程序包管理器控制台】中选择的项目是Cms.EntityFrameworkCore

然后执行数据库的迁移命令

1
2
Add-Migration createArticelAndArticelClass
Update-database

查看数据库。

2、文章展示布局

文章信息的展示,与用户信息的展示一样,也是通过表格进行展示。

创建控制器ArticelInfoController

针对该控制器中的index方法创建视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@{
Layout = null;
}
<html>
<head>
<script src="/lib/jquery/dist/jquery.min.js"></script>
<link href="/miniui/themes/default/miniui.css" type="text/css" rel="stylesheet" />
<link href="/miniui/themes/icons.css" type="text/css" rel="stylesheet" />
<link href="/miniui/themes/bootstrap/skin.css" type="text/css" rel="stylesheet" />
<script src="/js/miniui.js"></script>

</head>
<body>
<h1>文章信息管理</h1>

<div style="padding-bottom:5px;">

<span>文章标题:</span><input type="text" id="title" />
<span>文章作者:</span><input type="text" id="author" />
<input type="button" value="查找" onclick="search()" />

</div>
<div style="width:800px;">
<div class="mini-toolbar" style="border-bottom:0;padding:0px;">
<table style="width:100%;">
<tr>
<td style="width:100%;">
<a class="mini-button" iconCls="icon-add" onclick="add()">增加</a>
<a class="mini-button" iconCls="icon-remove" onclick="remove()">删除</a>
</td>
</tr>
</table>
</div>
</div>
<div id="datagrid1" class="mini-datagrid" style="width:60%;height:550px;" ajaxType="get" url="/articelInfo/getArticels"
idField="id" allowResize="true"
sizeList="[2,5,10,30]" pageSize="2"
showHeader="true" title="文章信息"
multiSelect="true"
allowCellEdit="true" allowCellSelect="true"
onmouseup="return datagrid1_onmouseup()">
<div property="columns">
<div type="indexcolumn"></div>
<div type="checkcolumn"></div>
<div field="id" width="120" headerAlign="center">编号</div>
<div field="Title" width="120" headerAlign="center">文章标题</div>
<div field="ClassName" align="right" width="100">文章类别</div>
<div field="Author" width="100" allowSort="true">文章作者</div>
<div field="Origin" width="100" headerAlign="center">文章来源</div>
<div field="CreateTime" width="100" headerAlign="center">发布时间</div>
<div name="action" width="120" headerAlign="center" align="center" cellStyle="padding:0;">操作</div>
</div>
</div>
</body>
</html>

3、添加文章视图

首先修改修改上面index.cshtml视图中的代码

当点击添加按钮的时候,会弹出一个窗口,在该窗口中呈现添加文章的表单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>

function add() {
mini.open({
targetWindow: window,
url: "/articelInfo/showAdd",
title: "添加文章", width: 800, height: 800,
onload: function () {
var iframe = this.getIFrameEl();
iframe.contentWindow;// 文档中这里调用了SetData方法,指的是在打开的页面中会有setData函数,而我们这里showAdd方法对应的视图中没有定义SetData函数,
//所以这里不用调用
},
ondestroy: function (action) {

grid.reload();
}
});
}
</script>

下面在ArticelInfo控制器中创建ShowAdd方法

1
2
3
4
public IActionResult ShowAdd()
{
return View();
}

该方法呈现的视图为ShowAdd.cshtml,该视图中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
@{
Layout = null;
}
<html>
<head>
<link href="/css/tableStyle.css" rel="stylesheet"/><!--添加表格样式-->
<script src="/ckeditor/ckeditor.js"></script><!--添加ckeditor编辑器--->
<script src="/lib/jquery/dist/jquery.min.js"></script>
<style type="text/css">
textarea, select {
padding: 2px;
border: 1px solid;
border-color: #666 #ccc #ccc #666;
background: #F9F9F9;
color: #333;
resize: none;
width: 100%;
}

.textbox {
padding: 3px;
border: 1px solid;
border-color: #666 #ccc #ccc #666;
background: #F9F9F9;
color: #333;
resize: none;
width: 100%;
}

.textbox:hover, .textbox:focus, textarea:hover, textarea:focus {
border-color: #09C;
background: #F5F9FD;
}
</style>
</head>
<body>
<table style="width:auto; margin: 0 auto">
<tr>
<td>简短标题:</td>
<td>
<select name="TitleType" id="TitleType">
<option></option>
<option style="color: green">[图文]</option>
<option style="color: red">[组图]</option>
<option style="color: #990000">[推荐]</option>
<option style="color: #0000FF">[注意]</option>
<option style="color: blue">[公告]</option>
</select>
</td>
<td>
<input type="text" name="title" maxlength="160" class="textbox" id="title" />
</td>
<td>
<select name="TitleFontType" id="TitleFontType">
<option value="0">字形</option>
<option value="1">粗体</option>
<option value="2">斜体</option>
<option value="3">粗+斜</option>
<option value="0">规则</option>
</select>
</td>
<td>
<select name="TitleFontColor" id="TitleFontColor">
<option value="0">颜色</option>
<option value="1">红色</option>
<option value="2">蓝色</option>
<option value="3">绿色</option>

</select>
</td>
</tr>
<tr>
<td>完整标题:</td>
<td colspan="4">
<input type="text" name="Fulltitle" class="textbox" id="Fulltitle" />
</td>
</tr>
<tr>
<td>归属栏目:</td>
<td colspan="4">
<select name="ArticelClassInfo" id="ArticelClassInfo">
<option value="">--请选择归属栏目--</option>

</select>
</td>
</tr>
<tr>
<td>关 键 字</td>
<td colspan="4">
<input type="text" name="KeyWords" class="textbox" id="KeyWords" />
</td>
</tr>
<tr>
<td>文章作者:</td>
<td colspan="4">
<input type="text" name="Author" class="textbox" id="Author" />&nbsp;&nbsp;<a href="javascript:void(0)" class="authorClick">未知</a>】【<a href="javascript:void(0)" class="authorClick">佚名</a>】【<a href="javascript:void(0)" class="authorClick">老王</a>
</td>
</tr>
<tr>
<td>文章来源:</td>
<td colspan="4">
<input name="Origin" id="Origin" value="" size="50" class="textbox" type="text">&nbsp;&nbsp;<a href="javascript:void(0)" class="originInfo">不详</a>】【<a href="javascript:void(0)" class="originInfo">本站原创</a>】【<a href="javascript:void(0)" class="originInfo">互联网</a>
</td>
</tr>
<tr>
<td>文章导读</td>
<td colspan="4">
<textarea class="textbox" name="Intro" style="width: 95%; height: 80px"></textarea>
</td>
</tr>
<tr>
<td>文章内容</td>
<td colspan="4">
<textarea class="textbox" onblur="getData2()" id="ArticleContent" name="ArticleContent1" rows="30" cols="40" style="width: 95%; height: 80px"></textarea>
<script type="text/javascript">
//<![CDATA[
// Replace the <textarea id="editor1"> with an CKEditor instance.
// 这里是给id属性值为ArticleContent的textare添加富文本编辑器
var editor = CKEDITOR.replace('ArticleContent');
//]]>
</script>

<textarea class="textbox" name="ArticleContent" id="txtArticleContent" rows="30" cols="40" style="display:none"></textarea>
</td>
</tr>
<tr>
<td>图片地址:</td>
<td colspan="4">
<div id="content">
<img id="imgSrc" width="50px" height="50px" />
</div>


<input name="PhotoUrl" id="PhotoUrl" value="" type="hidden">
<input name="AddWaterFlag" id="AddWaterFlag" value="1" type="checkbox">添加水印
<input name="InsertEditContent" id="InsertEditContent" value="1" type="checkbox">图片是否插入编辑器
</td>
</tr>
</table>
</body>
</html>

看一下基本的效果

同时,这里我们可以完成添加作者,添加原创,给富文本编辑器绑定一个事件的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$(function () {
//添加作者
$(".authorClick").click(function () {
$("#Author").val($(this).text());
});
//添加原创
$(".originInfo").click(function () {
$("#Origin").val($(this).text());
});
//给富文本编辑器绑定一个事件。
CKEDITOR.instances["ArticleContent"].on("blur", function () {
//获取编辑器内容。把用户在富文本编辑中添加的文章内容获取到以后,赋值给了id属性值为txtArticleContent的隐藏的文本域,最后提交的是该隐藏的文本域。
var oEditor = CKEDITOR.instances.ArticleContent;
var content = oEditor.getData();
$("#txtArticleContent").val(content);

});
});

4、上传图片并添加到富文本编辑器中。

导入axios.js文件

1
<script src="/js/axios.js"></script> <!--这里引入了axios.js-->

下面修改表格中的结构

1
2
3
4
5
6
7
8
<td>图片地址:</td>
<td colspan="4">
<div id="content">
<!--这里将文件上传域给隐藏了-->
<input class="mini-htmlfile" type="file" id="iptFile" accept="image/*" style="display:none" />
<button class="btn btn-primary" id="btnChoose">选择 & 上传图片</button>
<img id="imgSrc" width="50px" height="50px" />
</div>

下面是具体的js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
let btnChoose = document.querySelector('#btnChoose') // -----------获取上传按钮
let iptFile = document.querySelector('#iptFile') // -------------找到文件上传域
let imgsrc = document.querySelector('#imgSrc') // -------------注意:这里获取的是imgSrc找到展示头像图片的img标签

//-----------给上传按钮注册单击事件
btnChoose.addEventListener('click', function (e) {
e.preventDefault(); //----------------注意这里一定要阻止默认行为,因为按钮在表单中会有提交的动作。
iptFile.click()
})
// FormData 存文件 ==> axios 发请求 (看接口文档)
iptFile.addEventListener('change', function () {
// 用户选择的文件,该如何拿到?
// console.log(this.files[0])

// 当this.files[0] 是undefined的时候,表示用户未选中文件,以下代码不用执行
if (!this.files[0]) {
return // 阻止后续代码的执行
}

// FormData 构造函数 new一起使用
let fd = new FormData()
fd.append('avatar', this.files[0])

// axios 发请求代码
axios({
method: 'post',
// 请求的url地址一定要从接口文档中去复制
url: '/ArticelInfo/fileupload',
// data的值是fd(把FormData存的数据给发送到服务器上)
data: fd
}).then(({ data: res }) => {
console.log(res)

// 更换图片(以下写法有问题,res.url 缺少根路径)
// img.src = res.url // error
let photourl = document.getElementById("PhotoUrl");
photourl.value = res.url; // ------------------- 注意:将上传成功的头像的路径保存到隐藏域中
imgsrc.src = res.url

//----------------------------判断是否选择了"图片是否插入编辑器"
var flag = $("#InsertEditContent").is(":checked");
if (flag) {
var oEditor = CKEDITOR.instances.ArticleContent;//找到编辑器
if (oEditor.mode == 'wysiwyg') {
var img = "<img src='" + res.url + "'/>";
oEditor.insertHtml(img);//将上传成功的图片插入到编辑器中。
}
else
alert('You must be in WYSIWYG mode!');

}
})
})

下面要实现的ArticelInfo控制器中的FileUpload方法,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#region 图片上传
public async Task<IActionResult> FileUpload()
{
var file = Request.Form.Files["avatar"]; // 接收上传的图片文件
if (file == null) return Json(new { code = 500, msg = "请选择上传文件" }); // ----------注意:前端没有对返回的这块信息进行处理
string fileExtension = Path.GetExtension(file.FileName);
if(fileExtension !=".jpg") return Json(new { code = 500, msg = "只能上传.jpg格式的文件" });//---------------注意:前端没有对返回的这块信息进行处理
string fileName = Guid.NewGuid().ToString() + Path.GetExtension(file.FileName);
// 将上传的图片存储到不同的文件夹中,这里是以年月日作为文件夹名
string dir = "/ImageUp/" + DateTime.Now.Year + "/" + DateTime.Now.Month + "/" + DateTime.Now.Day + "/";
// 在wwwroot目录下面创建文件夹
Directory.CreateDirectory(Path.Combine(webHostEnvironment.ContentRootPath, "wwwroot" + dir));
// 构建文件存储的路径(一定要放在wwwroot目录下面)
var filePath = Path.Combine(webHostEnvironment.ContentRootPath, "wwwroot" + dir, fileName);

using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
await file.CopyToAsync(fileStream);
return Json(new { code = 200, url = dir + fileName }); // 将上传成功的图片路径进行返回
}
}
#endregion

启动项目进行测试。

这里也可以将上面的代码封装到服务层中。在对应的文章服务接口中定义如下方法

1
Task<string> FileUploadSave(IFormFile file,string rootPath);

注意:这里使用了IFormFile这个接口,所以需要导入using Microsoft.AspNetCore.Http

在具体的文章服务中实现上面的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public  async Task<string> FileUploadSave(IFormFile file,string rootPath)
{
string fileName = Guid.NewGuid().ToString() + Path.GetExtension(file.FileName);
// 将上传的图片存储到不同的文件夹中,这里是以年月日作为文件夹名
string dir = Path.Combine ( "ImageUp", DateTime.Now.Year.ToString(), DateTime.Now.Month.ToString(), DateTime.Now.Day.ToString());
// 在wwwroot目录下面创建文件夹
Directory.CreateDirectory(Path.Combine(rootPath, "wwwroot" , dir));
// 构建文件存储的路径(一定要放在wwwroot目录下面)
var filePath = Path.Combine(rootPath, "wwwroot", dir, fileName);

using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
await file.CopyToAsync(fileStream);
return dir + fileName;
}

}

文章控制器中的代码

1
2
3
4
5
6
7
8
9
10
11
public async Task<IActionResult> FileUpload()
{
var file = Request.Form.Files["avatar"]; // 接收上传的图片文件
if (file == null) return Json(new { code = 500, msg = "请选择上传文件" }); // ----------注意:前端没有对返回的这块信息进行处理
string fileExtension = Path.GetExtension(file.FileName);
if (fileExtension != ".jpg") return Json(new { code = 500, msg = "只能上传.jpg格式的文件" });//---------------注意:前端没有对返回的这块信息进行处理
string rootPath = webHostEnvironment.ContentRootPath;
string filePath = await articelInfo.FileUploadSave(file, rootPath);
return Json(new { code = 200, url = filePath });

}
1
2
3
4
5
6
7
private readonly IWebHostEnvironment webHostEnvironment;
private readonly IArticelInfoService articelInfoService;
public ArticelInfoController(IWebHostEnvironment webHostEnvironment, IUserInfoService asrticelInfoService)
{
this.webHostEnvironment = webHostEnvironment;
this.ArticelInfoService = articelInfoService;
}

在控制器的构造方法中进行注入。

5、添加水印

前端处理:

1
2
3
4
5
6
7
8
9
// axios 发请求代码
axios({
method: 'post',
// 请求的url地址一定要从接口文档中去复制
url: '/ArticelInfo/fileupload?warterFlag=' + document.getElementById("AddWaterFlag").value,
// data的值是fd(把FormData存的数据给发送到服务器上)
data: fd
}).then(({ data: res }) => {
console.log(res)

在向ArticelInfo控制器中的FileUpload方法发送请求的时候,添加了warterFlag参数,如果用户选择了[添加水印]这复选框,该参数是有值的。

下面修改ArticelInfo控制器中的FileUpload方法中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
if (!string.IsNullOrEmpty(Request.Query["warterFlag"])) //------ 接收warterFlag参数,判断是否有值
{

using (var stream = new MemoryStream()) //----- 创建内存流
{
//------ 将接收到的文件流拷贝到内存流中,内存流中存储了文件数据
await file.CopyToAsync(stream);
// ----根据内存流创建画布
using (Bitmap map = new Bitmap(stream))
{
// ------根据画布创建画笔
using (Graphics g = Graphics.FromImage(map))
{
// -----通过画笔在画布上写字
g.DrawString("老王", new Font("微软雅黑", 60.0f, FontStyle.Bold), Brushes.Red, new PointF(map.Width - 160, map.Height - 130));
// ---将画布的内容保存到fileStream这个文件流中,从而将图片保存到了指定的目录中
map.Save(fileStream, ImageFormat.Jpeg);
// ----------返回保存的图片路径
return Json(new { code = 200, url = dir + fileName });
}
}

}
}
else
{
// ----------如果没有选择添加水印,直接将文件保存到文件流中,从而保存到了指定的目录下面。
await file.CopyToAsync(fileStream);
return Json(new { code = 200, url = dir + fileName }); // 将上传成功的图片路径进行返回
}

}

启动项目进行测试。

如果封装到了业务层,这里需要修改一下文章服务接口

1
Task<string> FileUploadSave(IFormFile file,string rootPath,string warterFlag);

这里添加了参数warterFlag

文章服务类中具体代码的修改,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public async Task<string> FileUploadSave(IFormFile file, string rootPath, string warterFlag)
{
string fileName = Guid.NewGuid().ToString() + Path.GetExtension(file.FileName);
// 将上传的图片存储到不同的文件夹中,这里是以年月日作为文件夹名
string dir = Path.Combine("ImageUp", DateTime.Now.Year.ToString(), DateTime.Now.Month.ToString(), DateTime.Now.Day.ToString());
// 在wwwroot目录下面创建文件夹
Directory.CreateDirectory(Path.Combine(rootPath, "wwwroot", dir));
// 构建文件存储的路径(一定要放在wwwroot目录下面)
var filePath = Path.Combine(rootPath, "wwwroot", dir, fileName);

using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
// ---------------------判断参数warterFlag是否为空
if (!string.IsNullOrEmpty(warterFlag))
{
using (var stream = new MemoryStream()) //----- 创建内存流
{
//------ 将接收到的文件流拷贝到内存流中,内存流中存储了文件数据
await file.CopyToAsync(stream);
// ----根据内存流创建画布
using (Bitmap map = new Bitmap(stream))
{
// ------根据画布创建画笔
using (Graphics g = Graphics.FromImage(map))
{
// -----通过画笔在画布上写字
g.DrawString("老王", new Font("微软雅黑", 60.0f, FontStyle.Bold), Brushes.Red, new PointF(map.Width - 160, map.Height - 130));
// ---将画布的内容保存到fileStream这个文件流中,从而将图片保存到了指定的目录中
map.Save(fileStream, ImageFormat.Jpeg);
// ----------返回保存的图片路径

}
}

}
}
else
{
await file.CopyToAsync(fileStream);

}

return dir + fileName;


}

}

控制器中代码的修改

1
2
3
4
5
6
7
8
9
10
11
public async Task<IActionResult> FileUpload()
{
var file = Request.Form.Files["avatar"]; // 接收上传的图片文件
if (file == null) return Json(new { code = 500, msg = "请选择上传文件" }); // ----------注意:前端没有对返回的这块信息进行处理
string fileExtension = Path.GetExtension(file.FileName);
if (fileExtension != ".jpg") return Json(new { code = 500, msg = "只能上传.jpg格式的文件" });//---------------注意:前端没有对返回的这块信息进行处理
string rootPath = webHostEnvironment.ContentRootPath;
string filePath = await articelInfoService.FileUploadSave(file, rootPath, Request.Query["warterFlag"]); //这里传递了waterFlag参数
return Json(new { code = 200, url = filePath });

}

这里在调用articelInfoService服务类中的FileUploadSave方法的时候传递了Request.Query["warterFlag"]

6、加载文章分类

在添加文章的表单中,有一个下拉框,来展示文章的分类,下面实现文章分类的加载功能。

这里采用的是递归的方式,获取所有的文章分类(文章分类就是一个树形结构)。

当然,在页面的下拉框中进行展示的时候,我们只是展示前两层分类就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//将数组中的数据转换成树形数据(使用递归算法)
function tranListToTreeData(list, rootValue) { // 0
var arr = [];
list.forEach((item) => {
if (item.parentId === rootValue) {
// 找到之后 就要去找 item 下面有没有子节点
const children = tranListToTreeData(list, item.id);
if (children.length) {
// 如果children的长度大于0 说明找到了子节点
item.children = children;
}
arr.push(item); // 将内容加入到数组中
}
});
return arr;
}

7、页面静态化

Cms.Common 安装NVelocity模版引擎

1
Install-Package NVelocity

同时在该类库项目中创建NVelocityHelper类,该类中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// <summary>
/// 创建NVelocity模板引擎,读取设计好的模板页面,替换占位符。
/// </summary>
/// <param name="templateName"></param>
/// <param name="data"></param>
/// <param name="templateFilePath"></param>
/// <returns></returns>
public static string RenderTemplate(string templateName, object data, string templateFilePath)
{
VelocityEngine vltEngine = new VelocityEngine(); // 创建模版引擎
vltEngine.SetProperty(RuntimeConstants.RESOURCE_LOADER, "file");
vltEngine.SetProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, templateFilePath);
vltEngine.Init();

VelocityContext vltContext = new VelocityContext();
vltContext.Put("data", data); // 把data对象中的数据传递数据给data属性,在模版中会替换对应的占位符

Template vltTemplate = vltEngine.GetTemplate(templateName + ".html"); // 得到模版文件名称
System.IO.StringWriter vltWriter = new System.IO.StringWriter();
vltTemplate.Merge(vltContext, vltWriter);

return vltWriter.GetStringBuilder().ToString();
}

以上代码是固定的写法,直接使用

ArticelTemplate这个文件夹拷贝到Cms.WebUI项目中的wwwroot这个目录中。ArticelTemplate这个文件夹中存放的就是生成静态页面的模版文件。

返回到添加文章的视图页面ShowAdd.cshtml中,进行代码的修改:

1
2
<script src="/js/axios.js"></script> 
<script src="/js/form-serialize.js"></script> <!--这里引入了form-serialize.js-->

这里首先引入了form-serialize.js这个文件。

1
2
3
4
5
6
<body>
<form id="form1"> <!---------添加form标签---------->
<table style="width:auto; margin: 0 auto">
<tr>
<td>简短标题:</td>
<td>

这里使用form标签包裹了整个表格中的表单元素。

1
2
3
4
5
6
7
   
</table>
<div style="text-align:center;padding:10px;">
<button style="width:60px;margin-right:20px;" id="btnOk">确定</button>
<button onclick="onCancel()" style="width:60px;">取消</button>
</div>
</form>

同时在表格标签的后面添加了确定与取消的按钮。这里我们直接给确定按钮添加了一个id属性。

1
2
3
4
5
6
7
8
9
10
11
12
 $(function () {
//--------省略了以前的代码
// 给确定按钮注册了单击事件
$("#btnOk").click(function(e){
SaveData(e)
})
});
// 关闭窗口的方法
function onCancel() {

window.CloseOwnerWindow();
}

上面的代码中,我们给【确定】按钮注册了单击事件,事件触发以后,调用了SaveData方法来发送请求,这里传递了事件对象e.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 保存表单中的数据      
function SaveData(e) {
e.preventDefault(); // 阻止默认行为
var form = document.getElementById("form1");
let data = serialize(form); // 获取表单中的数据
$.ajax({
url: '/ArticelInfo/CreateArticel',
method: "post",
data: data,
success: function (data) {
onCancel();
},
error: function (error) {

}
})

}

在上面的代码中,获取了表单中的数据,然后通过ajax请求,发送到ArticelInfo这个控制器中的CreateArticel方法中。

下面实现该方法。代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#region 保存文章信息
[UnitOfWork(new Type[] { typeof(MyDbContext) })]
public IActionResult CreateArticel(ArticelInfo articelInfo)
{
articelInfo.CreateTime = DateTime.Now;
articelInfo.UpdateTime = DateTime.Now;
articelInfo.DelFlag =Convert.ToBoolean(DelFlagEnum.Normal);
// 这里需要特别注意的是,需要单独的接收下拉框中所选择的文章分类的编号
int cId =Convert.ToInt32(Request.Form["ArticelClassInfo"]);
// 调用CreateHtmlPage 方法,生成静态页面。
CreateHtmlPage(articelInfo,"add");
return Content("ok");
}

#endregion

在上面的代码中,我们还没有将提交过来的文章数据保存到数据库,这里只是做了相应的接收以后,调用了CreateHtmlPage方法生成了静态页面。

下面就直接创建CreateHtmlPage方法来生成静态页面,该方法的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#region 生成静态页面.
public async void CreateHtmlPage(ArticelInfo articelInfo, string flag)
{
// 调用RenderTemplate方法完成模版中占位符的替换,生成静态页面完整的数据
// 第一个参数:表示模版的名称
// 第二个参数:表示具体的数据,也就是使用该对象替换模版中的占位符
// 第三个参数,指定具体的模版文件的具体存放路径
string html = NVelocityHelper.RenderTemplate("ArticelTemplateInfo", articelInfo, Path.Combine(webHostEnvironment.ContentRootPath, "wwwroot/ArticelTemplate/"));
// 生成好的静态文件存放在该目录下面
string dir = "/ArticelHtml/" + articelInfo.CreateTime.Year + "/" + articelInfo.CreateTime.Month + "/" + articelInfo.CreateTime.Day + "/";
// 这里将生成好的静态文件也是存放在了wwwroot目录下的ArticelHtml目录中
string dirCombine= Path.Combine(webHostEnvironment.ContentRootPath, "wwwroot" + dir);
if (flag == "add")
{
// 创建指定的目录
Directory.CreateDirectory(dirCombine);
}

// 获取具体的静态文件完整的路径(包含文件名,文件名就是guid来命名的)
string fullDir = dirCombine + Guid.NewGuid().ToString().Substring(0,8) + ".html";
// html参数中存储的是通过NVelocity模版替换站位符以后得到的静态页面的完整数据
// fullDir是静态文件完整的存储路径
// 通过WriteAllTextAsync方法把html参数中的数据,写入到指定的目录下面
await System.IO.File.WriteAllTextAsync(fullDir, html, System.Text.Encoding.UTF8);

}
#endregion

启动程序进行测试。

可以在wwwroot目录下面看到一个ArticelHtml目录,在该目录下的指定的日期命名的文件夹中,可以看到一个0.html文件,这就是我们生成的具体的静态页面。这里文件名是0的原因是,我们还没有保存数据到数据库中,所以Id的值就是0。

8、完成文章信息的存储

指定数据仓储接口IArticelInfoRepository.cs

1
2
3
public interface IArticelInfoRepository:IBaseRepository<ArticelInfo>
{
}

创建具体的数据仓储实现ArticelInfoRepository.cs,如下所示:

1
2
3
4
5
6
7
8
namespace Cms.Repository
{
public class ArticelInfoRepository:BaseRepository<ArticelInfo>,IArticelInfoRepository
{
public ArticelInfoRepository(MyDbContext context) : base(context) { }
}
}

创建具体的服务接口IArticelInfoService.cs

1
2
3
4
5
6
namespace Cms.IService
{
public interface IArticelInfoService:IBaseService<ArticelInfo>
{
}
}

创建具体的服务实现类ArticelInfoService.cs

1
2
3
4
5
6
7
8
9
10
public class ArticelInfoService:BaseService<ArticelInfo>,IArticelInfoService
{
private readonly IArticelInfoRepository _articelRepository;
public ArticelInfoService(IArticelInfoRepository articelRepository)
{
base.repository = articelRepository;
_articelRepository = articelRepository;
}

}

下面完善控制器中的CreateArticel方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#region 保存文章信息
[UnitOfWork(new Type[] { typeof(MyDbContext) })]
public async Task<IActionResult> CreateArticel(ArticelInfo articelInfo)
{
articelInfo.CreateTime = DateTime.Now;
articelInfo.UpdateTime = DateTime.Now;
articelInfo.DelFlag =Convert.ToBoolean(DelFlagEnum.Normal);
int cId =Convert.ToInt32(Request.Form["ArticelClassInfo"]);

articelInfo.ArticelClassId = cId;


var articel = await articelInfoService.InsertEntityAsync(articelInfo);

CreateHtmlPage(articel, "add");

return Json(new { code = 200, msg = "添加成功", articelInfo });
}

这里我们接收到cId这个文章的类别编号以后,我们没有查询对应的类别信息,而是直接赋值给了articelInfo对象中表示外键的属性ArticelClassId属性。

这就要求我们指定这个外键,关于这一点,我们在学习EF Core的时候给大家讲解过。

这里我们需要修改一下Cms.Entity这个类库项目中的ArticelInfo.cs实体类

1
2
3
4
5
6
7
public string? PhotoUrl { get; set; }
/// <summary>
/// 文章属于哪个类别
/// </summary>
public ArticelClass? ArticelClass { get; set;}

public int ArticelClassId { get; set; } // 这里添加了外键的属性ArticelClassId

这里添加了外键的属性ArticelClassId

同时修改ArticelInfoConfig.cs这个配置文件,如下所示:

1
2
// 一个类别下面多篇文章
builder.HasOne<ArticelClass>(c => c.ArticelClass).WithMany(a => a.ArticelInfos).IsRequired().HasForeignKey(c=>c.ArticelClassId);

这里,通过HasForeignKey指定了外键是ArticelClassId.

这里,不需要重新进行数据库的迁移操作,因为在T_ArticelInfos表中,有对应的外键就是ArticelClassId.

1
2
3
4
5
6
7
#region 生成静态页面.
public async void CreateHtmlPage(ArticelInfo articelInfo, string flag)
{
if (articelInfo.TitleFontColor == "1")
{
articelInfo.Title = "<font color='red' >" + articelInfo.Title + "</font>";
}

在生成静态页面的时候,可以判断TitleFontColor属性的取值,如果是1,表示用户选择的标题颜色是红色。所以这里使用了font标签添加了红色。

其他的选项,大家可以自己进行判断。

9、敏感词过滤

在添加文章或者是发布评论的时候,需要对用户输入的内容进行审查,这里主要是审查一下是否有敏感词。

关于敏感词,主要分为审查词和禁用词。

审查词:如果文章内容中含有审查词,可以将文章内容存储到数据库中,但是需要管理员审查以后才能展示到前端页面中。

禁用词:直接不允许将文章内容存储到数据库中。

替换词:有些词可以进行替换。

把数据库中存储的词库取出来以后,存储到缓存中。

10、网站发布

可以在Cms.WebUI这个Web应用程序上右击,从弹出的快捷菜单中选择发布

选择发布目标的时候,可以选择【文件夹】,这样可以将项目中的文件发布到指定的文件夹下面,然后拷贝到服务器上。

我们可以单击【所有显示设置】,在弹出的设置的窗口中,进行设置

【配置】:选择Release,不要选择Debug,因为Release性能高,Debug中包含了很多的调试信息。

【目标框架】:这里我们选择.net 7.0

【部署模式】:关于“框架依赖”这个选项的含义:发布的项目是不包含.net运行时的,需要服务器上单独的安装.net的运行时。

​ “独立”:表示发布的程序中包含了.net的运行时。

【文件发布选项】:“生成单个文件”:表示的是把所有的.dll文件生成到一个文件中。“在发布前删除所有现有文件”:表示的是把用来存放发布项目对应的文件夹中原有的文件删除掉,保证该文件夹一直存储最新的发布后的文件。

“启用ReadyToRun 编译”这个选项:https://learn.microsoft.com/zh-cn/dotnet/core/deploying/ready-to-run

【数据库】选项中,不需要更改,因为以后,上线,需要更改成生成数据库的链接字符串。

当网站发布以后,我们双击对应的exe文件,就可以启动我们所发布的网站的原因是:在.net core中内置了Kestrel服务器,尽管该服务器已经非常的强大了,但是一般仍然不会让Kestrel直接面对用户的请求,

一般都是浏览器与Kestrel服务器之间会添加一个反向代理服务器。

添加反向代理服务器的好处:实现集群化配置。

当然,针对.net core有很多的部署:K8s+容器(推荐,难道高),Linux+NginxWindows+IIS

部署到IIS参考文档:https://blog.csdn.net/qq_16256429/article/details/122400717