无私分享:从入门到精通ASP.NET MVC|从0开始搭框架做项目(10)——部门管理、岗位管理和员工管理

在企业级Web系统中,部门-岗位-员工是最基础且最核心的业务模块组合。它们的关系贯穿了权限管理、流程审批、数据统计等几乎所有上层功能,掌握这套模块的开发逻辑,能帮你快速理解「关联数据处理」「树形结构展示」「文件上传」等高频需求的解决思路。

本章节将基于前9章的基础(如Model-View-Controller分层、Entity Framework Code First、依赖注入),完整实现这三个模块的增删改查「业务规则校验」「关联数据处理」「前端交互优化」,并融入通用仓储模式「Unit of Work事务管理」「RESTful接口设计」等最佳实践。

目录#

  1. 需求分析与数据模型设计
    • 1.1 业务关系梳理
    • 1.2 数据库表设计(Entity Framework Code First)
  2. 基础架构准备:通用CRUD与依赖注入
    • 2.1 通用仓储模式(Generic Repository)
    • 2.2 服务层抽象(IService/BaseService)
    • 2.3 依赖注入配置(Autofac)
  3. 部门管理模块实现
    • 3.1 部门Controller:RESTful风格接口设计
    • 3.2 部门视图:树形结构展示(ZTree)
    • 3.3 业务规则:部门层级校验(避免循环引用)
  4. 岗位管理模块实现
    • 4.1 岗位与部门的关联设计
    • 4.2 岗位Controller:批量操作与状态切换
    • 4.3 岗位视图:联动下拉选择部门
  5. 员工管理模块实现
    • 5.1 员工与部门/岗位的多对多关系
    • 5.2 员工Controller:文件上传(头像)与数据校验
    • 5.3 员工视图:表格分页与弹出层编辑
  6. 最佳实践总结
    • 6.1 数据一致性:事务处理(Unit of Work)
    • 6.2 性能优化:Eager Loading vs Lazy Loading
    • 6.3 安全防护:防止SQL注入与XSS攻击
  7. 常见问题排查与解决
    • 7.1 树形结构加载缓慢怎么办?
    • 7.2 关联数据保存失败如何调试?
    • 7.3 文件上传路径权限问题处理
  8. 下一步:扩展与优化建议
  9. 参考资料

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配置关系#

AppDbContextOnModelCreating方法中配置关联关系(避免数据冗余或循环删除):

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.DynamicProxy

2.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 业务规则:部门层级校验#

部门新增/编辑时需校验以下规则:

  1. 父部门必须存在且启用
  2. 不能将部门设置为自身或子部门的子节点(避免循环引用)。

DepartmentServiceAddAsync方法中实现校验:

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 岗位与部门的关联设计#

岗位创建时需选择所属部门,因此:

  • 后端PositionControllerCreate方法需获取所有启用的部门,传递给前端下拉框;
  • 前端使用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实现分页功能,步骤如下:

  1. 安装NuGet包:Install-Package PagedList.Mvc
  2. 后端EmployeeControllerIndex方法返回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);
    }
  3. 前端视图显示分页控件:
    @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实现弹出层编辑,步骤如下:

  1. 前端定义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">&times;</span></button>
                    <h4 class="modal-title">编辑员工</h4>
                </div>
                <div class="modal-body">
                    <!-- AJAX加载编辑表单 -->
                </div>
            </div>
        </div>
    </div>
  2. 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);
        });
    });
  3. 后端返回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目录的权限;
  • 解决
    1. 右键Uploads目录→属性→安全→编辑;
    2. 添加IIS AppPool\你的应用池名称(如IIS AppPool\MvcDemo);
    3. 授予「修改」权限。

8. 下一步:扩展与优化建议#

  1. 权限管理:集成ASP.NET Identity,实现「角色-权限」控制(如只有管理员能删除部门);
  2. Excel导出:使用EPPlus实现员工列表导出(参考EPPlus文档);
  3. 搜索功能:添加姓名、部门、岗位的模糊搜索(使用Where拼接条件);
  4. 异步操作:所有I/O操作(如SaveChangesAsync「GetAllAsync」)都使用async/await,提升并发性能。

参考资料#

  1. ASP.NET MVC 5 官方文档
  2. Entity Framework Code First 指南
  3. Autofac 文档
  4. ZTree 官方文档
  5. PagedList.Mvc
  6. EPPlus

结语#

通过本章节的学习,你已经掌握了企业级系统中最基础的三个模块的开发逻辑。实践是最好的学习方式——建议你基于本代码扩展功能(如添加员工的邮箱/电话字段、岗位的排序功能),并尝试用Postman测试RESTful接口,用Visual Studio Debugger排查问题。

下一章我们将进入权限管理模块(角色-权限-用户),敬请期待!