前言
距离上一篇已经有段时间了,最近这段时间赶上新项目开发,一直没有时间来写。之前的几篇文章,主要把EF的基础都讲了一遍,这批文章就来个实战篇。
个人在学习过程中参考博客:
- Entity Framework技术系列
- EF-Code First(1):Repository,UnitOfWork,DbContext
Repository
在数据库系统中,对于数据层来说,所有的操作归根结底无非“C(增加)、R(读取)、U(修改)、D(删除)”这四种操作。四种操作当中,与与业务相关度最大的是读取操作,根据各种不同的业务需求提交不同的查询,其最终执行应该放到业务层面中去进行,而增加,修改,删除这三种操作较为通用,可以作为通用数据操作封装到Repository中。在Repository中,唯一的变化点就是各种不同的实体类型,既然是变化点就应该进行封装,这里使用泛型来封装这个变化点。
还要说明一下,每个操作方法都带有一个 isSave 可选参数,是为了单个实体操作的需要,免去了每次都要调用 context.SaveChanged()的麻烦。如果是进行多个实体的单元事务操作,就需要把这个参数设置为 false 。
这里面就是定义了一个实体的增删改查。
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
|
/// <summary>
/// 定义仓储模型中的数据标准操作
/// </summary>
/// <typeparam name="TEntity">动态实体类型</typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
public
interface
IRepository<TEntity,
in
TKey> : IDependency
where
TEntity : EntityBase<TKey>
{
#region 属性
/// <summary>
/// 获取 当前实体的查询数据集
/// </summary>
IQueryable<TEntity> Entities {
get
; }
#endregion
#region 公共方法
/// <summary>
/// 插入实体记录
/// </summary>
/// <param name="entity"> 实体对象 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
int
Insert(TEntity entity,
bool
isSave =
true
);
/// <summary>
/// 批量插入实体记录集合
/// </summary>
/// <param name="entities"> 实体记录集合 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
int
Insert(IEnumerable<TEntity> entities,
bool
isSave =
true
);
/// <summary>
/// 删除指定编号的记录
/// </summary>
/// <param name="id"> 实体记录编号 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
int
Delete(TKey id,
bool
isSave =
true
);
/// <summary>
/// 删除实体记录
/// </summary>
/// <param name="entity"> 实体对象 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
int
Delete(TEntity entity,
bool
isSave =
true
);
/// <summary>
/// 删除实体记录集合
/// </summary>
/// <param name="entities"> 实体记录集合 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
int
Delete(IEnumerable<TEntity> entities,
bool
isSave =
true
);
/// <summary>
/// 删除所有符合特定表达式的数据
/// </summary>
/// <param name="predicate"> 查询条件谓语表达式 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
int
Delete(Expression<Func<TEntity,
bool
>> predicate,
bool
isSave =
true
);
/// <summary>
/// 更新实体记录
/// </summary>
/// <param name="entity"> 实体对象 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
int
Update(TEntity entity,
bool
isSave =
true
);
/// <summary>
/// 更新实体记录集合
/// </summary>
/// <param name="entity"> 实体对象 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
int
Update(IEnumerable<TEntity> entitys,
bool
isSave =
true
);
/// <summary>
/// 查找指定主键的实体记录
/// </summary>
/// <param name="key"> 指定主键 </param>
/// <returns> 符合编号的记录,不存在返回null </returns>
TEntity GetByKey(TKey key);
#endregion
}
|
Repository的通用实现如下:
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
|
/// <summary>
/// EntityFramework仓储操作基类
/// </summary>
/// <typeparam name="TEntity">动态实体类型</typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
public
abstract
class
EFRepositoryBase<TEntity, TKey> : IRepository<TEntity, TKey>
where
TEntity : EntityBase<TKey>
{
#region 属性
/// <summary>
/// 获取 仓储上下文的实例
/// </summary>
public
IUnitOfWork UnitOfWork {
get
;
set
; }
/// <summary>
/// 获取 EntityFramework的数据仓储上下文
/// </summary>
protected
UnitOfWorkContextBase EFContext
{
get
{
if
(UnitOfWork
is
UnitOfWorkContextBase)
{
return
UnitOfWork
as
UnitOfWorkContextBase;
}
throw
new
DataAccessException(
string
.Format(
"数据仓储上下文对象类型不正确,应为UnitOfWorkContextBase,实际为 {0}"
, UnitOfWork.GetType().Name));
}
}
/// <summary>
/// 获取 当前实体的查询数据集
/// </summary>
public
virtual
IQueryable<TEntity> Entities
{
get
{
return
EFContext.Set<TEntity, TKey>(); }
}
#endregion
#region 公共方法
/// <summary>
/// 插入实体记录
/// </summary>
/// <param name="entity"> 实体对象 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
public
virtual
int
Insert(TEntity entity,
bool
isSave =
true
)
{
PublicHelper.CheckArgument(entity,
"entity"
);
EFContext.RegisterNew<TEntity, TKey>(entity);
return
isSave ? EFContext.Commit() : 0;
}
/// <summary>
/// 批量插入实体记录集合
/// </summary>
/// <param name="entities"> 实体记录集合 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
public
virtual
int
Insert(IEnumerable<TEntity> entities,
bool
isSave =
true
)
{
PublicHelper.CheckArgument(entities,
"entities"
);
EFContext.RegisterNew<TEntity, TKey>(entities);
return
isSave ? EFContext.Commit() : 0;
}
/// <summary>
/// 删除指定编号的记录
/// </summary>
/// <param name="id"> 实体记录编号 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
public
virtual
int
Delete(TKey id,
bool
isSave =
true
)
{
PublicHelper.CheckArgument(id,
"id"
);
TEntity entity = EFContext.Set<TEntity, TKey>().Find(id);
return
entity !=
null
? Delete(entity, isSave) : 0;
}
/// <summary>
/// 删除实体记录
/// </summary>
/// <param name="entity"> 实体对象 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
public
virtual
int
Delete(TEntity entity,
bool
isSave =
true
)
{
PublicHelper.CheckArgument(entity,
"entity"
);
EFContext.RegisterDeleted<TEntity, TKey>(entity);
return
isSave ? EFContext.Commit() : 0;
}
/// <summary>
/// 删除实体记录集合
/// </summary>
/// <param name="entities"> 实体记录集合 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
public
virtual
int
Delete(IEnumerable<TEntity> entities,
bool
isSave =
true
)
{
PublicHelper.CheckArgument(entities,
"entities"
);
EFContext.RegisterDeleted<TEntity, TKey>(entities);
return
isSave ? EFContext.Commit() : 0;
}
/// <summary>
/// 删除所有符合特定表达式的数据
/// </summary>
/// <param name="predicate"> 查询条件谓语表达式 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
public
virtual
int
Delete(Expression<Func<TEntity,
bool
>> predicate,
bool
isSave =
true
)
{
PublicHelper.CheckArgument(predicate,
"predicate"
);
List<TEntity> entities = EFContext.Set<TEntity, TKey>().Where(predicate).ToList();
return
entities.Count > 0 ? Delete(entities, isSave) : 0;
}
/// <summary>
/// 更新实体记录
/// </summary>
/// <param name="entity"> 实体对象 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
public
virtual
int
Update(TEntity entity,
bool
isSave =
true
)
{
PublicHelper.CheckArgument(entity,
"entity"
);
EFContext.RegisterModified<TEntity, TKey>(entity);
return
isSave ? EFContext.Commit() : 0;
}
/// <summary>
/// 批量注册一个更改的对象到仓储上下文中
/// </summary>
/// <typeparam name="TEntity"> 要注册的类型 </typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
/// <param name="entity"> 要注册的对象 </param>
public
virtual
int
Update(IEnumerable<TEntity> entities,
bool
isSave =
true
)
{
PublicHelper.CheckArgument(entities,
"entities"
);
EFContext.RegisterModified<TEntity, TKey>(entities);
return
isSave ? EFContext.Commit() : 0;
}
/// <summary>
/// 查找指定主键的实体记录
/// </summary>
/// <param name="key"> 指定主键 </param>
/// <returns> 符合编号的记录,不存在返回null </returns>
public
virtual
TEntity GetByKey(TKey key)
{
PublicHelper.CheckArgument(key,
"key"
);
return
EFContext.Set<TEntity, TKey>().Find(key);
}
#endregion
}
|
实现类中所有操作最终都是通过单元操作来提交的,关于单元操作,接下来。这里用到了IOC,不懂的可以看之前的文章
UnitOfWork
引入单元操作,主要是为了给各个实体维护一个共同的DbContext上下文对象,保证所有的操作都是在共同的上下文中进行的。EF的操作提交 context.SaveChanged() 默认就是事务性的,只要保证了当前的所有实体的操作都是在一个共同的上下文中进行的,就实现了事务操作了。
在业务层中,各个实体的增删改操作都是通过各个实体的Repository进行的,只需要提供一个提交保存的功能作为最后调用,即可保证当前的提交是事务性的。因此定义给业务层引用的单元操作接口如下:
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
|
/// <summary>
/// 业务单元操作接口
/// </summary>
public
interface
IUnitOfWork : IDependency
{
#region 属性
/// <summary>
/// 获取 当前单元操作是否已被提交
/// </summary>
bool
IsCommitted {
get
; }
#endregion
#region 方法
/// <summary>
/// 提交当前单元操作的结果
/// </summary>
/// <param name="validateOnSaveEnabled">保存时是否自动验证跟踪实体</param>
/// <returns></returns>
int
Commit(
bool
validateOnSaveEnabled =
true
);
/// <summary>
/// 把当前单元操作回滚成未提交状态
/// </summary>
void
Rollback();
#endregion
}
|
在数据组件内部,数据操作最终都提交到一个与IUnitOfWork接口的实现类中进行操作,以保证各个实体的Repository与IUnitOfWork使用的是同一个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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
/// <summary>
/// 数据单元操作接口
/// </summary>
public
interface
IUnitOfWorkContext : IUnitOfWork, IDisposable
{
/// <summary>
/// 注册一个新的对象到仓储上下文中
/// </summary>
/// <typeparam name="TEntity"> 要注册的类型 </typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
/// <param name="entity"> 要注册的对象 </param>
void
RegisterNew<TEntity, TKey>(TEntity entity)
where
TEntity : EntityBase<TKey>;
/// <summary>
/// 批量注册多个新的对象到仓储上下文中
/// </summary>
/// <typeparam name="TEntity"> 要注册的类型 </typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
/// <param name="entities"> 要注册的对象集合 </param>
void
RegisterNew<TEntity, TKey>(IEnumerable<TEntity> entities)
where
TEntity : EntityBase<TKey>;
/// <summary>
/// 注册一个更改的对象到仓储上下文中
/// </summary>
/// <typeparam name="TEntity"> 要注册的类型 </typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
/// <param name="entity"> 要注册的对象 </param>
void
RegisterModified<TEntity, TKey>(TEntity entity)
where
TEntity : EntityBase<TKey>;
/// <summary>
/// 注册一个删除的对象到仓储上下文中
/// </summary>
/// <typeparam name="TEntity"> 要注册的类型 </typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
/// <param name="entity"> 要注册的对象 </param>
void
RegisterDeleted<TEntity, TKey>(TEntity entity)
where
TEntity : EntityBase<TKey>;
/// <summary>
/// 批量注册多个删除的对象到仓储上下文中
/// </summary>
/// <typeparam name="TEntity"> 要注册的类型 </typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
/// <param name="entities"> 要注册的对象集合 </param>
void
RegisterDeleted<TEntity, TKey>(IEnumerable<TEntity> entities)
where
TEntity : EntityBase<TKey>;
}
|
在单元操作的实现基类中,定义一个只读的DbContext抽象属性,实际的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
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
|
/// <summary>
/// 单元操作实现基类
/// </summary>
public
abstract
class
UnitOfWorkContextBase : IUnitOfWorkContext
{
/// <summary>
/// 获取 当前使用的数据访问上下文对象
/// </summary>
protected
abstract
DbContext Context {
get
; }
/// <summary>
/// 获取 当前单元操作是否已被提交
/// </summary>
public
bool
IsCommitted {
get
;
private
set
; }
public
DbContext DbContext {
get
{
return
Context; } }
/// <summary>
/// 提交当前单元操作的结果
/// </summary>
/// <param name="validateOnSaveEnabled">保存时是否自动验证跟踪实体</param>
/// <returns></returns>
public
int
Commit(
bool
validateOnSaveEnabled =
true
)
{
if
(IsCommitted)
{
return
0;
}
try
{
int
result = Context.SaveChanges(validateOnSaveEnabled);
IsCommitted =
true
;
return
result;
}
catch
(DbUpdateException e)
{
throw
;
}
}
/// <summary>
/// 把当前单元操作回滚成未提交状态
/// </summary>
public
void
Rollback()
{
IsCommitted =
false
;
}
public
void
Dispose()
{
//if (!IsCommitted)
//{
// Commit();
//}
Context.Dispose();
}
/// <summary>
/// 为指定的类型返回 System.Data.Entity.DbSet,这将允许对上下文中的给定实体执行 CRUD 操作。
/// </summary>
/// <typeparam name="TEntity"> 应为其返回一个集的实体类型。 </typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
/// <returns> 给定实体类型的 System.Data.Entity.DbSet 实例。 </returns>
public
DbSet<TEntity> Set<TEntity, TKey>()
where
TEntity : EntityBase<TKey>
{
return
Context.Set<TEntity>();
}
/// <summary>
/// 注册一个新的对象到仓储上下文中
/// </summary>
/// <typeparam name="TEntity"> 要注册的类型 </typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
/// <param name="entity"> 要注册的对象 </param>
public
void
RegisterNew<TEntity, TKey>(TEntity entity)
where
TEntity : EntityBase<TKey>
{
EntityState state = Context.Entry(entity).State;
if
(state == EntityState.Detached)
{
Context.Entry(entity).State = EntityState.Added;
}
IsCommitted =
false
;
}
/// <summary>
/// 批量注册多个新的对象到仓储上下文中
/// </summary>
/// <typeparam name="TEntity"> 要注册的类型 </typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
/// <param name="entities"> 要注册的对象集合 </param>
public
void
RegisterNew<TEntity, TKey>(IEnumerable<TEntity> entities)
where
TEntity : EntityBase<TKey>
{
try
{
//禁用自动发现功能
Context.Configuration.AutoDetectChangesEnabled =
false
;
foreach
(TEntity entity
in
entities)
{
RegisterNew<TEntity, TKey>(entity);
}
}
finally
{
Context.Configuration.AutoDetectChangesEnabled =
true
;
}
}
/// <summary>
/// 注册一个更改的对象到仓储上下文中
/// </summary>
/// <typeparam name="TEntity"> 要注册的类型 </typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
/// <param name="entity"> 要注册的对象 </param>
public
void
RegisterModified<TEntity, TKey>(TEntity entity)
where
TEntity : EntityBase<TKey>
{
Context.Update<TEntity, TKey>(entity);
IsCommitted =
false
;
}
/// <summary>
/// 批量注册一个更改的对象到仓储上下文中
/// </summary>
/// <typeparam name="TEntity"> 要注册的类型 </typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
/// <param name="entity"> 要注册的对象 </param>
public
void
RegisterModified<TEntity, TKey>(IEnumerable<TEntity> entities)
where
TEntity : EntityBase<TKey>
{
Context.Update<TEntity, TKey>(entities.ToArray());
IsCommitted =
false
;
}
/// <summary>
/// 注册一个删除的对象到仓储上下文中
/// </summary>
/// <typeparam name="TEntity"> 要注册的类型 </typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
/// <param name="entity"> 要注册的对象 </param>
public
void
RegisterDeleted<TEntity, TKey>(TEntity entity)
where
TEntity : EntityBase<TKey>
{
Context.Entry(entity).State = EntityState.Deleted;
IsCommitted =
false
;
}
/// <summary>
/// 批量注册多个删除的对象到仓储上下文中
/// </summary>
/// <typeparam name="TEntity"> 要注册的类型 </typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
/// <param name="entities"> 要注册的对象集合 </param>
public
void
RegisterDeleted<TEntity, TKey>(IEnumerable<TEntity> entities)
where
TEntity : EntityBase<TKey>
{
try
{
Context.Configuration.AutoDetectChangesEnabled =
false
;
foreach
(TEntity entity
in
entities)
{
RegisterDeleted<TEntity, TKey>(entity);
}
}
finally
{
Context.Configuration.AutoDetectChangesEnabled =
true
;
}
}
}
|
业务整合
首先看接口的定义 ,他需要继承仓储接口
1
2
3
4
|
public
interface
IUserService : IRepository<Models.User,
int
>
{
}
|
实现类继承仓储的基类
1
2
3
4
5
6
7
8
9
|
/// <summary>
/// 仓储操作层实现——登录记录信息
/// </summary>
public
partial
class
UserRepository : EFRepositoryBase<Models.User,
int
>, IUserService
{
}
}
|
主要看一下使用,这里使用的是属性注入,然后就可以调用了
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
|
public
class
UserService : IDependency
{
public
IUserService userService {
get
;
set
; }
/// <summary>
/// 获取 当前实体的查询数据集
/// </summary>
public
virtual
IQueryable<Models.User> User
{
get
{
return
userService.Entities; }
}
public
Models.User GetByKey(
int
id)
{
try
{
}
catch
(Exception)
{
throw
new
BusinessException();
}
return
userService.GetByKey(id);
}
/// <summary>
/// 批量插入实体记录集合
/// </summary>
/// <param name="entities"> 实体记录集合 </param>
/// <param name="isSave"> 是否执行保存 </param>
/// <returns> 操作影响的行数 </returns>
public
int
Insert(IEnumerable<Models.User> entities)
{
return
userService.Insert(entities);
}
/// <summary>
/// 注册一个更改的对象到仓储上下文中
/// </summary>
/// <typeparam name="TEntity"> 要注册的类型 </typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
/// <param name="entity"> 要注册的对象 </param>
public
int
RegisterModified(Models.User entity)
{
return
userService.Update(entity);
}
/// <summary>
/// 注册一个更改的对象到仓储上下文中
/// </summary>
/// <typeparam name="TEntity"> 要注册的类型 </typeparam>
/// <typeparam name="TKey">实体主键类型</typeparam>
/// <param name="entity"> 要注册的对象 </param>
public
int
RegisterModified(IEnumerable<Models.User> entity)
{
return
userService.Update(entity);
}
}
|
前端页面 OK完成,。
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
|
public
UserService userserver {
get
;
set
; }
public
ActionResult Index()
{
//Models.User user = userserver.GetByKey(1);
//ViewBag.Name = user.UserName;
//1、批量新增
//userserver.Insert(new List<Models.User> {
// new Models.User() { UserName="张三",Password="123456" },
// new Models.User() { UserName="李四",Password="123456" }
//});
//
//2、普通更新
//Models.User user = userserver.User.Single(m => m.UserID == 1);
//user.UserName = "张三1";
userserver.RegisterModified(user);
//Models.User user1 = userserver.User.Single(m => m.UserID == 2);
//user1.Password = "456789";
批量更新
//userserver.RegisterModified(new List<Models.User>
//{
// user, user1});
return
View();
}
|
结束语
本文最难理解的就是这个思路,如果能看懂的肯定会发现不足的地方,后面也准备了一个升级篇。不懂的可以加入下面的QQ群进行交流,源代码也已经上传到群共享文件了。欢迎下载。
作者:STONE刘先生 出处:http://www.cnblogs.com/liupeng/