无私分享:从入门到精通ASP.NET MVC|从0开始搭框架做项目(10)——部门管理、岗位管理和员工管理
在企业级Web系统中,部门-岗位-员工是最基础且最核心的业务模块组合。它们的关系贯穿了权限管理、流程审批、数据统计等几乎所有上层功能,掌握这套模块的开发逻辑,能帮你快速理解「关联数据处理」「树形结构展示」「文件上传」等高频需求的解决思路。
本章节将基于前9章的基础(如Model-View-Controller分层、Entity Framework Code First、依赖注入),完整实现这三个模块的增删改查「业务规则校验」「关联数据处理」「前端交互优化」,并融入通用仓储模式「Unit of Work事务管理」「RESTful接口设计」等最佳实践。
目录#
- 需求分析与数据模型设计
- 1.1 业务关系梳理
- 1.2 数据库表设计(Entity Framework Code First)
- 基础架构准备:通用CRUD与依赖注入
- 2.1 通用仓储模式(Generic Repository)
- 2.2 服务层抽象(IService/BaseService)
- 2.3 依赖注入配置(Autofac)
- 部门管理模块实现
- 3.1 部门Controller:RESTful风格接口设计
- 3.2 部门视图:树形结构展示(ZTree)
- 3.3 业务规则:部门层级校验(避免循环引用)
- 岗位管理模块实现
- 4.1 岗位与部门的关联设计
- 4.2 岗位Controller:批量操作与状态切换
- 4.3 岗位视图:联动下拉选择部门
- 员工管理模块实现
- 5.1 员工与部门/岗位的多对多关系
- 5.2 员工Controller:文件上传(头像)与数据校验
- 5.3 员工视图:表格分页与弹出层编辑
- 最佳实践总结
- 6.1 数据一致性:事务处理(Unit of Work)
- 6.2 性能优化:Eager Loading vs Lazy Loading
- 6.3 安全防护:防止SQL注入与XSS攻击
- 常见问题排查与解决
- 7.1 树形结构加载缓慢怎么办?
- 7.2 关联数据保存失败如何调试?
- 7.3 文件上传路径权限问题处理
- 下一步:扩展与优化建议
- 参考资料
1. 需求分析与数据模型设计#
1.1 业务关系梳理#
三个模块的核心关系如下:
- 部门(Department):树形层级结构(如「总公司→技术部→后端组」),支持启用/禁用。
- 岗位(Position):属于某个部门(如「技术部→Java开发工程师」),支持批量启用/禁用。
- 员工(Employee):
- 归属一个部门(多对一);
- 可担任多个岗位(多对多,如「后端开发+架构师」);
- 支持上传头像。
用ER图表示:
Department ──1对多── Position
Department ──1对多── Employee
Employee ──多对多── Position
1.2 数据库表设计(Entity Framework Code First)#
使用Code First模式定义模型(优先写C#类,再生成数据库表),并通过Fluent API配置关系。
1.2.1 模型类定义#
// 部门模型(自引用树形结构)
public class Department
{
public int Id { get; set; }
[Required(ErrorMessage = "部门名称不能为空")]
[StringLength(50, ErrorMessage = "部门名称最长50字符")]
public string Name { get; set; }
public int? ParentId { get; set; } // 父部门ID( nullable表示根部门)
public bool IsEnabled { get; set; } = true; // 是否启用
// 导航属性
public virtual Department Parent { get; set; } // 父部门
public virtual ICollection<Department> Children { get; set; } = new List<Department>(); // 子部门
public virtual ICollection<Position> Positions { get; set; } = new List<Position>(); // 关联岗位
}
// 岗位模型
public class Position
{
public int Id { get; set; }
[Required(ErrorMessage = "岗位名称不能为空")]
[StringLength(50)]
public string Name { get; set; }
public int DepartmentId { get; set; } // 所属部门ID
public bool IsEnabled { get; set; } = true;
// 导航属性
public virtual Department Department { get; set; }
public virtual ICollection<Employee> Employees { get; set; } = new List<Employee>(); // 关联员工
}
// 员工模型
public class Employee
{
public int Id { get; set; }
[Required(ErrorMessage = "员工姓名不能为空")]
[StringLength(50)]
public string Name { get; set; }
[StringLength(10)]
public string Gender { get; set; } // 男/女/其他
[DataType(DataType.Date, ErrorMessage = "出生日期格式错误")]
public DateTime BirthDate { get; set; }
public int DepartmentId { get; set; } // 所属部门ID
public string AvatarPath { get; set; } // 头像路径(相对路径)
// 导航属性
public virtual Department Department { get; set; }
public virtual ICollection<Position> Positions { get; set; } = new List<Position>(); // 关联岗位
}1.2.2 Fluent API配置关系#
在AppDbContext的OnModelCreating方法中配置关联关系(避免数据冗余或循环删除):
public class AppDbContext : DbContext
{
public AppDbContext() : base("DefaultConnection") { }
public DbSet<Department> Departments { get; set; }
public DbSet<Position> Positions { get; set; }
public DbSet<Employee> Employees { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// 1. 部门自引用关系(父-子)
modelBuilder.Entity<Department>()
.HasOptional(d => d.Parent) // 父部门可选(根部门无父部门)
.WithMany(d => d.Children) // 一个父部门对应多个子部门
.HasForeignKey(d => d.ParentId)
.WillCascadeOnDelete(false); // 禁止删除父部门时自动删除子部门
// 2. 员工与岗位的多对多关系(中间表EmployeePosition)
modelBuilder.Entity<Employee>()
.HasMany(e => e.Positions)
.WithMany(p => e.Employees)
.Map(m =>
{
m.ToTable("EmployeePosition"); // 中间表名
m.MapLeftKey("EmployeeId"); // 员工ID
m.MapRightKey("PositionId"); // 岗位ID
});
base.OnModelCreating(modelBuilder);
}
}2. 基础架构准备:通用CRUD与依赖注入#
为了避免重复代码(每个模块都写一遍增删改查),我们需要搭建通用数据访问层和服务层抽象。
2.1 通用仓储模式(Generic Repository)#
通用仓储封装了DbSet的基础操作,减少重复代码,同时隔离数据访问细节。
2.1.1 仓储接口(IRepository)#
public interface IRepository<TEntity> where TEntity : class
{
// 主键查询
TEntity GetById(int id);
// 异步主键查询
Task<TEntity> GetByIdAsync(int id);
// 获取全部数据(返回IQueryable支持延迟执行)
IQueryable<TEntity> GetAll();
// 条件查询
IQueryable<TEntity> Find(Expression<Func<TEntity, bool>> predicate);
// 新增
void Add(TEntity entity);
// 批量新增
void AddRange(IEnumerable<TEntity> entities);
// 更新
void Update(TEntity entity);
// 删除
void Delete(TEntity entity);
// 按主键删除
void Delete(int id);
}2.1.2 仓储实现(Repository)#
public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
protected readonly AppDbContext _context;
protected readonly DbSet<TEntity> _dbSet;
public Repository(AppDbContext context)
{
_context = context;
_dbSet = context.Set<TEntity>();
}
public TEntity GetById(int id) => _dbSet.Find(id);
public async Task<TEntity> GetByIdAsync(int id) => await _dbSet.FindAsync(id);
public IQueryable<TEntity> GetAll() => _dbSet.AsQueryable();
public IQueryable<TEntity> Find(Expression<Func<TEntity, bool>> predicate) => _dbSet.Where(predicate);
public void Add(TEntity entity) => _dbSet.Add(entity);
public void AddRange(IEnumerable<TEntity> entities) => _dbSet.AddRange(entities);
public void Update(TEntity entity)
{
_dbSet.Attach(entity);
_context.Entry(entity).State = EntityState.Modified;
}
public void Delete(TEntity entity)
{
if (_context.Entry(entity).State == EntityState.Detached)
_dbSet.Attach(entity);
_dbSet.Remove(entity);
}
public void Delete(int id) => Delete(GetById(id));
}2.2 服务层抽象(IService/BaseService)#
服务层负责业务规则校验和关联数据处理,依赖仓储层实现具体操作。
2.2.1 服务接口(IService)#
public interface IService<TEntity> where TEntity : class
{
Task<TEntity> GetByIdAsync(int id);
Task<IEnumerable<TEntity>> GetAllAsync(bool includeDisabled = false);
Task AddAsync(TEntity entity);
Task UpdateAsync(TEntity entity);
Task DeleteAsync(int id);
}2.2.2 基础服务实现(BaseService)#
public class BaseService<TEntity> : IService<TEntity> where TEntity : class
{
protected readonly IRepository<TEntity> _repository;
public BaseService(IRepository<TEntity> repository)
{
_repository = repository;
}
public async Task<TEntity> GetByIdAsync(int id) => await _repository.GetByIdAsync(id);
public async Task<IEnumerable<TEntity>> GetAllAsync(bool includeDisabled = false)
{
var query = _repository.GetAll();
if (!includeDisabled && typeof(TEntity).GetProperty("IsEnabled") != null)
{
// 过滤未启用的数据(仅针对有IsEnabled属性的实体)
query = query.Where(e => (bool)typeof(TEntity).GetProperty("IsEnabled").GetValue(e));
}
return await query.ToListAsync();
}
public async Task AddAsync(TEntity entity)
{
_repository.Add(entity);
await _context.SaveChangesAsync(); // 注意:后续会用Unit of Work统一管理事务
}
// 其他方法类似,此处省略...
}2.3 依赖注入配置(Autofac)#
使用Autofac将仓储「服务」「DbContext」注册到容器,实现依赖注入。
2.3.1 安装NuGet包#
Install-Package Autofac.Mvc5
Install-Package Autofac.Extras.DynamicProxy2.3.2 注册容器(Global.asax.cs)#
protected void Application_Start()
{
// 1. 创建容器构建器
var builder = new ContainerBuilder();
// 2. 注册MVC控制器(按程序集扫描)
builder.RegisterControllers(typeof(MvcApplication).Assembly);
// 3. 注册DbContext(生命周期:请求范围)
builder.RegisterType<AppDbContext>().AsSelf().InstancePerRequest();
// 4. 注册仓储(泛型)
builder.RegisterGeneric(typeof(Repository<>)).As(typeof(IRepository<>)).InstancePerRequest();
// 5. 注册服务(按程序集扫描)
builder.RegisterAssemblyTypes(typeof(DepartmentService).Assembly)
.Where(t => t.Name.EndsWith("Service"))
.AsImplementedInterfaces()
.InstancePerRequest();
// 6. 构建容器并设置MVC依赖解析器
var container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
// 其他MVC初始化代码...
}3. 部门管理模块实现#
部门模块的核心是树形结构展示和层级关系校验,我们使用ZTree实现前端树形组件,后端提供RESTful接口返回树结构数据。
3.1 部门Controller:RESTful风格接口设计#
遵循RESTful规范,用HTTP动词区分操作类型:
GET /Department:获取部门列表(树形)GET /Department/Create:打开新增页面POST /Department:提交新增部门GET /Department/Edit/{id}:打开编辑页面PUT /Department/{id}:提交编辑(需配合[HttpPut])
public class DepartmentController : Controller
{
private readonly IDepartmentService _departmentService;
// 构造函数注入服务
public DepartmentController(IDepartmentService departmentService)
{
_departmentService = departmentService;
}
// GET: 部门列表页
public ActionResult Index() => View();
// GET: 新增部门页面
public async Task<ActionResult> Create()
{
// 获取所有启用的部门作为父部门选项
var departments = await _departmentService.GetAllAsync();
ViewBag.ParentId = new SelectList(departments, "Id", "Name");
return View();
}
// POST: 提交新增部门
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(Department department)
{
if (ModelState.IsValid)
{
try
{
await _departmentService.AddAsync(department);
return RedirectToAction("Index");
}
catch (ArgumentException ex)
{
ModelState.AddModelError("", ex.Message); // 显示业务错误
}
}
// 重新加载父部门选项
ViewBag.ParentId = new SelectList(await _departmentService.GetAllAsync(), "Id", "Name", department.ParentId);
return View(department);
}
// AJAX: 获取部门树形结构(用于ZTree)
[HttpGet]
public async Task<JsonResult> GetDepartmentTree()
{
var treeData = await _departmentService.GetDepartmentTreeAsync();
return Json(treeData, JsonRequestBehavior.AllowGet);
}
}3.2 部门视图:树形结构展示(ZTree)#
使用ZTree实现树形组件,通过AJAX加载后端返回的树结构数据。
3.2.1 引入ZTree资源#
在Layout.cshtml中引入ZTree的CSS和JS:
<link rel="stylesheet" href="~/Scripts/ztree/css/zTreeStyle/zTreeStyle.css" />
<script src="~/Scripts/ztree/jquery.ztree.all.min.js"></script>3.2.2 视图页面(Index.cshtml)#
@{ ViewBag.Title = "部门管理"; }
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">部门树形结构</h3>
<a href="@Url.Action("Create")" class="btn btn-primary btn-sm pull-right">新增部门</a>
</div>
<div class="panel-body">
<div id="departmentTree" class="ztree"></div>
</div>
</div>
@section Scripts {
<script>
$(function () {
// ZTree配置(简单数据模式)
var setting = {
data: {
simpleData: {
enable: true, // 启用简单数据格式(pId关联父节点)
pIdKey: "parentId" // 父节点ID的字段名
}
},
callback: {
onClick: function (event, treeId, treeNode) {
// 点击节点触发的操作(如加载子部门的岗位)
console.log("点击了部门:" + treeNode.name);
}
}
};
// AJAX加载树数据
$.getJSON("@Url.Action("GetDepartmentTree")", function (data) {
$.fn.zTree.init($("#departmentTree"), setting, data);
});
});
</script>
}3.2.3 后端树结构数据构造#
在DepartmentService中实现GetDepartmentTreeAsync方法,递归构建树形结构:
public async Task<IEnumerable<DepartmentTreeDto>> GetDepartmentTreeAsync()
{
var departments = await _repository.GetAll()
.Where(d => d.IsEnabled)
.ToListAsync();
// 递归构建树结构(根部门:ParentId为null)
var rootDepartments = departments.Where(d => !d.ParentId.HasValue);
return BuildTree(rootDepartments, departments);
}
// 递归构建子节点
private List<DepartmentTreeDto> BuildTree(IEnumerable<Department> parentDepartments, List<Department> allDepartments)
{
var treeNodes = new List<DepartmentTreeDto>();
foreach (var parent in parentDepartments)
{
var node = new DepartmentTreeDto
{
id = parent.Id,
name = parent.Name,
parentId = parent.ParentId,
children = BuildTree(allDepartments.Where(d => d.ParentId == parent.Id), allDepartments)
};
treeNodes.Add(node);
}
return treeNodes;
}
// DTO:用于返回树形结构数据(避免暴露实体全部属性)
public class DepartmentTreeDto
{
public int id { get; set; }
public string name { get; set; }
public int? parentId { get; set; }
public List<DepartmentTreeDto> children { get; set; }
}3.3 业务规则:部门层级校验#
部门新增/编辑时需校验以下规则:
- 父部门必须存在且启用;
- 不能将部门设置为自身或子部门的子节点(避免循环引用)。
在DepartmentService的AddAsync方法中实现校验:
public async Task AddAsync(Department department)
{
// 1. 校验父部门
if (department.ParentId.HasValue)
{
var parent = await _repository.GetByIdAsync(department.ParentId.Value);
if (parent == null || !parent.IsEnabled)
throw new ArgumentException("父部门不存在或已禁用");
}
// 2. 校验循环引用(递归检查父部门链)
if (await IsCircularReferenceAsync(department.Id, department.ParentId ?? 0))
throw new ArgumentException("不能将部门设置为自身或子部门的子节点");
_repository.Add(department);
await _unitOfWork.SaveChangesAsync();
}
// 递归检查循环引用
private async Task<bool> IsCircularReferenceAsync(int departmentId, int parentId)
{
if (parentId == 0) return false;
var parent = await _repository.GetByIdAsync(parentId);
if (parent == null) return false;
// 如果父部门是当前部门,存在循环引用
if (parent.Id == departmentId) return true;
// 递归检查父部门的父部门
return await IsCircularReferenceAsync(departmentId, parent.ParentId ?? 0);
}4. 岗位管理模块实现#
岗位依赖于部门(岗位→部门是多对一关系),核心需求是联动选择部门和批量操作。
4.1 岗位与部门的关联设计#
岗位创建时需选择所属部门,因此:
- 后端
PositionController的Create方法需获取所有启用的部门,传递给前端下拉框; - 前端使用
Html.DropDownListFor绑定DepartmentId字段。
4.2 岗位Controller:批量操作与状态切换#
4.2.1 批量启用/禁用岗位#
通过复选框+批量按钮实现,后端接收岗位ID列表,批量更新IsEnabled状态:
[HttpPost]
public async Task<JsonResult> BatchUpdateStatus(List<int> positionIds, bool isEnabled)
{
try
{
var positions = await _repository.Find(p => positionIds.Contains(p.Id)).ToListAsync();
foreach (var position in positions)
{
position.IsEnabled = isEnabled;
_repository.Update(position);
}
await _unitOfWork.SaveChangesAsync();
return Json(new { success = true, message = "操作成功" });
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}4.2.2 状态切换(单个岗位)#
使用Bootstrap Toggle组件实现开关切换,通过AJAX更新状态:
<!-- 前端开关组件 -->
<input type="checkbox" class="status-toggle" data-id="@position.Id" @(position.IsEnabled ? "checked" : "")>
<!-- AJAX请求 -->
<script>
$(".status-toggle").change(function () {
var positionId = $(this).data("id");
var isEnabled = $(this).is(":checked");
$.post("@Url.Action("UpdateStatus")", { positionId: positionId, isEnabled: isEnabled }, function (res) {
if (res.success) {
alert("状态更新成功");
} else {
alert("状态更新失败:" + res.message);
}
});
});
</script>4.3 岗位视图:联动下拉选择部门#
在岗位创建页面,使用Html.DropDownListFor绑定部门下拉框:
// PositionController的Create方法
public async Task<ActionResult> Create()
{
var departments = await _departmentService.GetAllAsync();
ViewBag.DepartmentId = new SelectList(departments, "Id", "Name");
return View();
}<!-- 岗位创建视图(Create.cshtml) -->
<div class="form-group">
@Html.LabelFor(model => model.DepartmentId, "所属部门", new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownListFor(model => model.DepartmentId, ViewBag.DepartmentId as SelectList, "请选择部门", new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.DepartmentId)
</div>
</div>5. 员工管理模块实现#
员工模块的核心是多对多关系处理(员工→岗位)和文件上传(头像),我们使用PagedList.Mvc实现分页,Bootstrap Modal实现弹出层编辑。
5.1 员工与部门/岗位的多对多关系#
员工可以担任多个岗位,因此:
- 前端使用复选框选择岗位;
- 后端接收岗位ID列表,通过
Employee.Positions关联数据。
5.2 员工Controller:文件上传(头像)与数据校验#
5.2.1 头像上传实现#
员工模型中的AvatarPath存储相对路径(如~/Uploads/Avatars/20240501_123456.jpg),上传逻辑如下:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(Employee employee, HttpPostedFileBase avatar)
{
if (ModelState.IsValid)
{
try
{
// 1. 处理头像上传
if (avatar != null && avatar.ContentLength > 0)
{
var uploadDir = Server.MapPath("~/Uploads/Avatars");
if (!Directory.Exists(uploadDir))
Directory.CreateDirectory(uploadDir);
// 生成唯一文件名(避免覆盖)
var fileName = $"{DateTime.Now:yyyyMMdd_HHmmss}_{Path.GetFileNameWithoutExtension(avatar.FileName)}{Path.GetExtension(avatar.FileName)}";
var filePath = Path.Combine(uploadDir, fileName);
avatar.SaveAs(filePath);
// 保存相对路径到数据库
employee.AvatarPath = $"~/Uploads/Avatars/{fileName}";
}
// 2. 处理岗位关联(接收前端传递的岗位ID列表)
var positionIds = Request.Form.GetValues("PositionIds")?.Select(int.Parse) ?? new List<int>();
employee.Positions = await _positionService.GetByIdsAsync(positionIds);
// 3. 保存员工数据
await _employeeService.AddAsync(employee);
return RedirectToAction("Index");
}
catch (ArgumentException ex)
{
ModelState.AddModelError("", ex.Message);
}
}
// 重新加载部门和岗位选项
ViewBag.DepartmentId = new SelectList(await _departmentService.GetAllAsync(), "Id", "Name", employee.DepartmentId);
ViewBag.PositionIds = new MultiSelectList(await _positionService.GetAllAsync(), "Id", "Name");
return View(employee);
}5.2.2 数据校验#
使用数据注解和ModelState实现前端+后端双重校验:
- 前端:
Html.ValidationMessageFor显示错误提示; - 后端:
ModelState.IsValid判断数据是否合法。
例如,员工姓名的必填校验:
public class Employee
{
[Required(ErrorMessage = "员工姓名不能为空")]
[StringLength(50, ErrorMessage = "姓名最长50字符")]
public string Name { get; set; }
}前端显示错误:
<div class="form-group">
@Html.LabelFor(model => model.Name, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Name, new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
</div>
</div>5.3 员工视图:表格分页与弹出层编辑#
5.3.1 表格分页(PagedList.Mvc)#
使用PagedList.Mvc实现分页功能,步骤如下:
- 安装NuGet包:
Install-Package PagedList.Mvc; - 后端
EmployeeController的Index方法返回IPagedList<Employee>:public async Task<ActionResult> Index(int? page) { var pageNumber = page ?? 1; // 当前页码(默认第一页) var pageSize = 10; // 每页显示10条 var employees = await _employeeService.GetAllAsync() .OrderBy(e => e.Name) .ToPagedListAsync(pageNumber, pageSize); return View(employees); } - 前端视图显示分页控件:
@using PagedList.Mvc; <table class="table table-bordered"> <thead> <tr> <th>姓名</th> <th>性别</th> <th>出生日期</th> <th>部门</th> <th>操作</th> </tr> </thead> <tbody> @foreach (var employee in Model) { <tr> <td>@employee.Name</td> <td>@employee.Gender</td> <td>@employee.BirthDate.ToString("yyyy-MM-dd")</td> <td>@employee.Department.Name</td> <td> <a href="#" class="btn btn-sm btn-primary" data-toggle="modal" data-target="#editModal" data-id="@employee.Id">编辑</a> <a href="@Url.Action("Delete", new { id = employee.Id })" class="btn btn-sm btn-danger">删除</a> </td> </tr> } </tbody> </table> <!-- 分页控件 --> @Html.PagedListPager(Model, page => Url.Action("Index", new { page }), new PagedListRenderOptions { Display = PagedListDisplayMode.IfNeeded })
5.3.2 弹出层编辑(Bootstrap Modal)#
使用Bootstrap Modal实现弹出层编辑,步骤如下:
- 前端定义Modal:
<div class="modal fade" id="editModal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> <h4 class="modal-title">编辑员工</h4> </div> <div class="modal-body"> <!-- AJAX加载编辑表单 --> </div> </div> </div> </div> - AJAX加载编辑表单:
$("#editModal").on("show.bs.modal", function (event) { var button = $(event.relatedTarget); // 触发Modal的按钮 var employeeId = button.data("id"); // 获取员工ID // AJAX加载编辑表单(PartialView) $.get("@Url.Action("EditPartial")?id=" + employeeId, function (data) { $("#editModal .modal-body").html(data); }); }); - 后端返回PartialView:
public async Task<PartialViewResult> EditPartial(int id) { var employee = await _employeeService.GetByIdAsync(id); var departments = await _departmentService.GetAllAsync(); var positions = await _positionService.GetAllAsync(); ViewBag.DepartmentId = new SelectList(departments, "Id", "Name", employee.DepartmentId); ViewBag.PositionIds = new MultiSelectList(positions, "Id", "Name", employee.Positions.Select(p => p.Id)); return PartialView("_EditPartial", employee); }
6. 最佳实践总结#
6.1 数据一致性:事务处理(Unit of Work)#
当需要同时操作多个表(如新增员工+关联岗位)时,需用Unit of Work统一管理事务,确保数据一致性。
Unit of Work实现#
public interface IUnitOfWork : IDisposable
{
IRepository<Department> DepartmentRepository { get; }
IRepository<Position> PositionRepository { get; }
IRepository<Employee> EmployeeRepository { get; }
Task<int> SaveChangesAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
private IRepository<Department> _departmentRepository;
private IRepository<Position> _positionRepository;
private IRepository<Employee> _employeeRepository;
public UnitOfWork(AppDbContext context)
{
_context = context;
}
public IRepository<Department> DepartmentRepository => _departmentRepository ??= new Repository<Department>(_context);
public IRepository<Position> PositionRepository => _positionRepository ??= new Repository<Position>(_context);
public IRepository<Employee> EmployeeRepository => _employeeRepository ??= new Repository<Employee>(_context);
public async Task<int> SaveChangesAsync() => await _context.SaveChangesAsync();
public void Dispose() => _context.Dispose();
}使用Unit of Work管理事务#
public class EmployeeService : BaseService<Employee>
{
private readonly IUnitOfWork _unitOfWork;
public EmployeeService(IUnitOfWork unitOfWork) : base(unitOfWork.EmployeeRepository)
{
_unitOfWork = unitOfWork;
}
public async Task AddAsync(Employee employee, List<int> positionIds)
{
// 1. 新增员工
_unitOfWork.EmployeeRepository.Add(employee);
// 2. 关联岗位
var positions = await _unitOfWork.PositionRepository.GetAll()
.Where(p => positionIds.Contains(p.Id))
.ToListAsync();
employee.Positions = positions;
// 3. 统一提交事务
await _unitOfWork.SaveChangesAsync();
}
}6.2 性能优化:Eager Loading vs Lazy Loading#
- Eager Loading(预先加载):使用
Include方法一次性加载关联数据,避免N+1查询(推荐)。// 加载员工的部门和岗位(预先加载) var employees = await _unitOfWork.EmployeeRepository.GetAll() .Include(e => e.Department) .Include(e => e.Positions) .ToListAsync(); - Lazy Loading(延迟加载):默认启用(需导航属性为
virtual),但会导致多次数据库查询(不推荐在列表页使用)。
6.3 安全防护:防止SQL注入与XSS攻击#
- SQL注入:Entity Framework的LINQ查询会自动参数化,避免直接拼接SQL(禁止使用
ExecuteSqlCommand执行未参数化的SQL)。 - XSS攻击:Razor视图默认会对输出的字符串进行HTML编码(如
@Model.Name会自动转义<script>标签)。如需显示富文本,使用Html.Raw并配合HtmlSanitizer过滤危险标签:// 安装HtmlSanitizer:Install-Package HtmlSanitizer var sanitizer = new HtmlSanitizer(); var safeHtml = sanitizer.Sanitize(unsafeHtml);
7. 常见问题排查与解决#
7.1 树形结构加载缓慢怎么办?#
- 原因:一次性加载所有部门(包括子部门),数据量过大;
- 解决:使用ZTree的异步加载(仅加载展开节点的子部门):
var setting = { async: { enable: true, // 启用异步加载 url: "@Url.Action("GetChildDepartments")", // 加载子部门的接口 autoParam: ["id"], // 自动传递当前节点的ID dataFilter: function (treeId, parentNode, responseData) { return responseData; // 处理返回的子部门数据 } } };
7.2 关联数据保存失败如何调试?#
- 检查导航属性:确保
Employee.Positions已正确赋值(非null); - 检查中间表:使用SQL Server Profiler查看生成的SQL语句,确认中间表
EmployeePosition是否插入了数据; - 检查事务:确保使用Unit of Work统一提交(
SaveChangesAsync只调用一次)。
7.3 文件上传路径权限问题处理#
- 原因:IIS应用池没有写入
Uploads目录的权限; - 解决:
- 右键
Uploads目录→属性→安全→编辑; - 添加
IIS AppPool\你的应用池名称(如IIS AppPool\MvcDemo); - 授予「修改」权限。
- 右键
8. 下一步:扩展与优化建议#
- 权限管理:集成ASP.NET Identity,实现「角色-权限」控制(如只有管理员能删除部门);
- Excel导出:使用
EPPlus实现员工列表导出(参考EPPlus文档); - 搜索功能:添加姓名、部门、岗位的模糊搜索(使用
Where拼接条件); - 异步操作:所有I/O操作(如
SaveChangesAsync「GetAllAsync」)都使用async/await,提升并发性能。
参考资料#
结语#
通过本章节的学习,你已经掌握了企业级系统中最基础的三个模块的开发逻辑。实践是最好的学习方式——建议你基于本代码扩展功能(如添加员工的邮箱/电话字段、岗位的排序功能),并尝试用Postman测试RESTful接口,用Visual Studio Debugger排查问题。
下一章我们将进入权限管理模块(角色-权限-用户),敬请期待!