概述
本文档通过职工管理(Employee)的实际代码示例,展示如何使用CodeSpirit框架快速开发CRUD功能。该示例来自身份认证系统(IdentityApi),是一个标准的关联型CRUD模块,包含完整的验证逻辑、业务处理和关联关系管理。
最后更新: 2025年12月22日
框架版本: v2.0.0
示例来源: CodeSpirit.IdentityApi - 职工管理模块
开发流程概览
graph LR A["1. 创建实体模型"] --> B["2. 创建DTO类"] B --> C["3. 配置AutoMapper"] C --> D["4. 创建服务层"] D --> E["5. 创建控制器"] E --> F["6. 配置数据库"] F --> G["7. 创建迁移"] G --> H["完成"]示例模块说明
职工管理(Employee)是一个典型的关联型CRUD模块,具有以下特点:
- ✅ 关联关系管理(部门、用户账号)
- ✅ 完整的CRUD操作
- ✅ 业务验证(工号唯一性、部门存在性、身份证格式等)
- ✅ 多条件查询(关键字、部门、状态、日期范围等)
- ✅ 表单分组展示(基本信息、联系方式、工作信息等)
- ✅ 多租户支持
- ✅ 审计字段自动记录
- ✅ 软删除支持
1. 创建实体模型
在Data/Models目录下创建实体类:说明:
- 实现IFullAuditable接口,自动包含完整的审计字段(创建、更新、删除)
- 实现IMultiTenant接口,支持多租户数据隔离
- 实现IIsActive接口,支持激活状态管理
- 使用long作为主键类型
- 包含关联关系的导航属性(部门、用户账号)
- 支持软删除(IsDeleted字段)
2. 创建DTO类
在Dtos/Employee目录下创建DTO类:
2.1 EmployeeDto(展示DTO)
说明:
列特性(Columns):用于控制前端表格列的显示和格式
- AmisColumn:基础列特性,控制列的显示、排序、隐藏等
- Hidden:是否隐藏列
- Sortable:是否支持排序
- Copyable:是否可复制
- Fixed:是否固定列(left/right/none)
- StatusMapping:状态映射(支持预定义映射如Boolean、HttpStatusCode等)
- TplColumn:自定义列显示模板,使用模板语法自定义列内容
- template:模板字符串,支持变量插值(如${name})
- AvatarColumn:头像列,显示头像图片
- Text:头像下方显示的文本
- Src:头像图片地址
- DateColumn:日期列,格式化日期显示
- Format:日期格式(如YYYY-MM-DD、YYYY-MM-DD HH:mm)
- FromNow:是否显示相对时间(如"2小时前")
- IgnoreColumn:忽略列,该字段不在表格中显示
- TagsColumn:标签列,以标签形式显示数组数据
- LinkColumn:链接列,显示可点击的链接
- AmisStatusColumn:状态列,显示状态标签和图标
- LongTextColumn:长文本列,支持展开/收起
- ListColumn:列表列,显示列表数据
- IconColumn:图标列,显示图标
2.2 CreateEmployeeDto(创建DTO)
- // Dtos/Employee/CreateEmployeeDto.cs
- using CodeSpirit.Amis.Attributes.FormFields;
- using CodeSpirit.IdentityApi.Data.Models;
- using System.ComponentModel;
- using System.ComponentModel.DataAnnotations;
- namespace CodeSpirit.IdentityApi.Dtos.Employee;
- /// <summary>
- /// 创建职工数据传输对象
- /// </summary>
- [FormGroup("basic", "基本信息", "EmployeeNo,Name,Gender,IdNo,BirthDate", Order = 1)]
- [FormGroup("contact", "联系方式", "PhoneNumber,Email,Address", Order = 2)]
- [FormGroup("work", "工作信息", "DepartmentId,Position,JobLevel,HireDate,EmploymentStatus", Order = 3)]
- [FormGroup("relation", "关联信息", "UserId", Order = 4)]
- [FormGroup("emergency", "紧急联系人", "EmergencyContact,EmergencyPhone", Order = 5)]
- [FormGroup("other", "其他信息", "AvatarUrl,Remarks,IsActive", Order = 6)]
- public class CreateEmployeeDto
- {
- /// <summary>
- /// 工号
- /// </summary>
- [Required(ErrorMessage = "工号不能为空")]
- [MaxLength(50, ErrorMessage = "工号长度不能超过50个字符")]
- [DisplayName("工号")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string EmployeeNo { get; set; } = string.Empty;
- /// <summary>
- /// 姓名
- /// </summary>
- [Required(ErrorMessage = "姓名不能为空")]
- [MaxLength(100, ErrorMessage = "姓名长度不能超过100个字符")]
- [DisplayName("姓名")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string Name { get; set; } = string.Empty;
- /// <summary>
- /// 性别
- /// </summary>
- [DisplayName("性别")]
- [AmisFormField(ColumnRatio = 6)]
- public Gender Gender { get; set; }
- /// <summary>
- /// 身份证号码
- /// </summary>
- [MaxLength(18, ErrorMessage = "身份证号码长度不能超过18个字符")]
- [DisplayName("身份证号")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string? IdNo { get; set; }
- /// <summary>
- /// 出生日期
- /// </summary>
- [DisplayName("出生日期")]
- [AmisDateFieldAttribute(ColumnRatio = 6)]
- public DateTime? BirthDate { get; set; }
- /// <summary>
- /// 手机号码
- /// </summary>
- [MaxLength(15, ErrorMessage = "手机号码长度不能超过15个字符")]
- [Phone(ErrorMessage = "手机号码格式不正确")]
- [DisplayName("手机号码")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string? PhoneNumber { get; set; }
- /// <summary>
- /// 电子邮箱
- /// </summary>
- [MaxLength(100, ErrorMessage = "电子邮箱长度不能超过100个字符")]
- [EmailAddress(ErrorMessage = "电子邮箱格式不正确")]
- [DisplayName("电子邮箱")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string? Email { get; set; }
- /// <summary>
- /// 部门ID
- /// </summary>
- [DisplayName("部门")]
- [AmisInputTreeField(
- DataSource = "${ROOT_API}/api/identity/Departments/tree",
- LabelField = "name",
- ValueField = "id",
- Multiple = false,
- Searchable = true,
- ColumnRatio = 12
- )]
- public long? DepartmentId { get; set; }
- /// <summary>
- /// 职位
- /// </summary>
- [MaxLength(100, ErrorMessage = "职位长度不能超过100个字符")]
- [DisplayName("职位")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string? Position { get; set; }
- /// <summary>
- /// 职级
- /// </summary>
- [MaxLength(50, ErrorMessage = "职级长度不能超过50个字符")]
- [DisplayName("职级")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string? JobLevel { get; set; }
- /// <summary>
- /// 入职日期
- /// </summary>
- [DisplayName("入职日期")]
- [AmisDateFieldAttribute(ColumnRatio = 6)]
- public DateTime? HireDate { get; set; }
- /// <summary>
- /// 在职状态
- /// </summary>
- [DisplayName("在职状态")]
- [AmisFormField(ColumnRatio = 6)]
- public EmploymentStatus EmploymentStatus { get; set; } = EmploymentStatus.Active;
- /// <summary>
- /// 关联的用户ID
- /// </summary>
- [DisplayName("关联用户")]
- [AmisSelectField(
- Source = "${ROOT_API}/api/identity/Users",
- ValueField = "id",
- LabelField = "name",
- Multiple = false,
- Searchable = true,
- ColumnRatio = 12
- )]
- public long? UserId { get; set; }
- /// <summary>
- /// 紧急联系人
- /// </summary>
- [MaxLength(100, ErrorMessage = "紧急联系人长度不能超过100个字符")]
- [DisplayName("紧急联系人")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string? EmergencyContact { get; set; }
- /// <summary>
- /// 紧急联系电话
- /// </summary>
- [MaxLength(15, ErrorMessage = "紧急联系电话长度不能超过15个字符")]
- [Phone(ErrorMessage = "紧急联系电话格式不正确")]
- [DisplayName("紧急联系电话")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string? EmergencyPhone { get; set; }
- /// <summary>
- /// 地址
- /// </summary>
- [MaxLength(500, ErrorMessage = "地址长度不能超过500个字符")]
- [DisplayName("地址")]
- [AmisTextareaField(ColumnRatio = 12)]
- public string? Address { get; set; }
- /// <summary>
- /// 头像地址
- /// </summary>
- [MaxLength(255, ErrorMessage = "头像地址长度不能超过255个字符")]
- [DisplayName("头像")]
- [AmisInputImageField(
- Receiver = "/file/api/file/images/upload?BucketName=avatar",
- Accept = "image/png,image/jpeg,image/jpg",
- MaxSize = 2097152,
- Multiple = false,
- ColumnRatio = 12
- )]
- public string? AvatarUrl { get; set; }
- /// <summary>
- /// 备注
- /// </summary>
- [MaxLength(1000, ErrorMessage = "备注长度不能超过1000个字符")]
- [DisplayName("备注")]
- [AmisTextareaField(ColumnRatio = 12)]
- public string? Remarks { get; set; }
- /// <summary>
- /// 是否激活
- /// </summary>
- [DisplayName("是否激活")]
- [AmisFormField(ColumnRatio = 6)]
- public bool IsActive { get; set; } = true;
- }
复制代码 说明:
表单特性(FormFields):用于控制前端表单字段的显示和交互
- FormGroup:表单分组特性,将相关字段组织成组
- Name:组名称
- Title:组标题
- Fields:包含的字段名称(逗号分隔)
- Order:显示顺序(数值越小越靠前)
- Mode:显示模式(Normal/Inline/Horizontal)
- AmisInputTextField:文本输入框
- ColumnRatio:字段宽度比例(12为全宽,6为半宽)
- EnableAddOn:是否启用右侧附加组件
- AddOnLabel:附加组件标签
- AddOnApi:附加组件API地址
- AmisInputTreeField:树形选择组件
- DataSource:数据源URL
- ValueField:值字段名
- LabelField:标签字段名
- Multiple:是否多选
- Searchable:是否可搜索
- ShowOutline:是否显示轮廓
- SubmitOnChange:选择后是否自动提交
- AmisSelectField:下拉选择组件
- Source:数据源URL
- ValueField:值字段名
- LabelField:标签字段名
- Multiple:是否多选
- Searchable:是否可搜索
- Clearable:是否可清除
- AmisInputImageField:图片上传组件
- Receiver:上传接口地址
- Accept:接受的文件类型
- MaxSize:最大文件大小(字节)
- Multiple:是否支持多文件
- AmisDateFieldAttribute:日期选择组件
- Format:日期格式
- Placeholder:占位符
- MinDate:最小日期
- MaxDate:最大日期
- AmisTextareaField:多行文本输入框
- MaxLength:最大长度
- ShowCounter:是否显示字符计数
- Rows:行数
通用属性:
- ColumnRatio:字段宽度比例(12为全宽,6为半宽,4为1/3宽)
- Required:是否必填
- Placeholder:占位符文本
- Disabled:是否禁用
- VisibleOn:显示条件表达式
- DisabledOn:禁用条件表达式
2.3 UpdateEmployeeDto(更新DTO)
- // Dtos/Employee/UpdateEmployeeDto.cs
- using CodeSpirit.Amis.Attributes.FormFields;
- using CodeSpirit.IdentityApi.Data.Models;
- using System.ComponentModel;
- using System.ComponentModel.DataAnnotations;
- namespace CodeSpirit.IdentityApi.Dtos.Employee;
- /// <summary>
- /// 更新职工数据传输对象
- /// </summary>
- [FormGroup("basic", "基本信息", "EmployeeNo,Name,Gender,IdNo,BirthDate", Order = 1)]
- [FormGroup("contact", "联系方式", "PhoneNumber,Email,Address", Order = 2)]
- [FormGroup("work", "工作信息", "DepartmentId,Position,JobLevel,HireDate,TerminationDate,EmploymentStatus", Order = 3)]
- [FormGroup("relation", "关联信息", "UserId", Order = 4)]
- [FormGroup("emergency", "紧急联系人", "EmergencyContact,EmergencyPhone", Order = 5)]
- [FormGroup("other", "其他信息", "AvatarUrl,Remarks,IsActive", Order = 6)]
- public class UpdateEmployeeDto
- {
- /// <summary>
- /// 工号
- /// </summary>
- [Required(ErrorMessage = "工号不能为空")]
- [MaxLength(50, ErrorMessage = "工号长度不能超过50个字符")]
- [DisplayName("工号")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string EmployeeNo { get; set; } = string.Empty;
- /// <summary>
- /// 姓名
- /// </summary>
- [Required(ErrorMessage = "姓名不能为空")]
- [MaxLength(100, ErrorMessage = "姓名长度不能超过100个字符")]
- [DisplayName("姓名")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string Name { get; set; } = string.Empty;
- /// <summary>
- /// 性别
- /// </summary>
- [DisplayName("性别")]
- [AmisFormField(ColumnRatio = 6)]
- public Gender Gender { get; set; }
- /// <summary>
- /// 身份证号码
- /// </summary>
- [MaxLength(18, ErrorMessage = "身份证号码长度不能超过18个字符")]
- [DisplayName("身份证号")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string? IdNo { get; set; }
- /// <summary>
- /// 出生日期
- /// </summary>
- [DisplayName("出生日期")]
- [AmisDateFieldAttribute(ColumnRatio = 6)]
- public DateTime? BirthDate { get; set; }
- /// <summary>
- /// 手机号码
- /// </summary>
- [MaxLength(15, ErrorMessage = "手机号码长度不能超过15个字符")]
- [Phone(ErrorMessage = "手机号码格式不正确")]
- [DisplayName("手机号码")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string? PhoneNumber { get; set; }
- /// <summary>
- /// 电子邮箱
- /// </summary>
- [MaxLength(100, ErrorMessage = "电子邮箱长度不能超过100个字符")]
- [EmailAddress(ErrorMessage = "电子邮箱格式不正确")]
- [DisplayName("电子邮箱")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string? Email { get; set; }
- /// <summary>
- /// 部门ID
- /// </summary>
- [DisplayName("部门")]
- [AmisInputTreeField(
- DataSource = "${ROOT_API}/api/identity/Departments/tree",
- LabelField = "name",
- ValueField = "id",
- Multiple = false,
- Searchable = true,
- ColumnRatio = 12
- )]
- public long? DepartmentId { get; set; }
- /// <summary>
- /// 职位
- /// </summary>
- [MaxLength(100, ErrorMessage = "职位长度不能超过100个字符")]
- [DisplayName("职位")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string? Position { get; set; }
- /// <summary>
- /// 职级
- /// </summary>
- [MaxLength(50, ErrorMessage = "职级长度不能超过50个字符")]
- [DisplayName("职级")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string? JobLevel { get; set; }
- /// <summary>
- /// 入职日期
- /// </summary>
- [DisplayName("入职日期")]
- [AmisDateFieldAttribute(ColumnRatio = 6)]
- public DateTime? HireDate { get; set; }
- /// <summary>
- /// 离职日期
- /// </summary>
- [DisplayName("离职日期")]
- [AmisDateFieldAttribute(ColumnRatio = 6)]
- public DateTime? TerminationDate { get; set; }
- /// <summary>
- /// 在职状态
- /// </summary>
- [DisplayName("在职状态")]
- [AmisFormField(ColumnRatio = 12)]
- public EmploymentStatus EmploymentStatus { get; set; }
- /// <summary>
- /// 关联的用户ID
- /// </summary>
- [DisplayName("关联用户")]
- [AmisSelectField(
- Source = "${ROOT_API}/api/identity/Users",
- ValueField = "id",
- LabelField = "name",
- Multiple = false,
- Searchable = true,
- ColumnRatio = 12
- )]
- public long? UserId { get; set; }
- /// <summary>
- /// 紧急联系人
- /// </summary>
- [MaxLength(100, ErrorMessage = "紧急联系人长度不能超过100个字符")]
- [DisplayName("紧急联系人")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string? EmergencyContact { get; set; }
- /// <summary>
- /// 紧急联系电话
- /// </summary>
- [MaxLength(15, ErrorMessage = "紧急联系电话长度不能超过15个字符")]
- [Phone(ErrorMessage = "紧急联系电话格式不正确")]
- [DisplayName("紧急联系电话")]
- [AmisInputTextField(ColumnRatio = 6)]
- public string? EmergencyPhone { get; set; }
- /// <summary>
- /// 地址
- /// </summary>
- [MaxLength(500, ErrorMessage = "地址长度不能超过500个字符")]
- [DisplayName("地址")]
- [AmisTextareaField(ColumnRatio = 12)]
- public string? Address { get; set; }
- /// <summary>
- /// 头像地址
- /// </summary>
- [MaxLength(255, ErrorMessage = "头像地址长度不能超过255个字符")]
- [DisplayName("头像")]
- [AmisInputImageField(
- Receiver = "/file/api/file/images/upload?BucketName=avatar",
- Accept = "image/png,image/jpeg,image/jpg",
- MaxSize = 2097152,
- Multiple = false,
- ColumnRatio = 12
- )]
- public string? AvatarUrl { get; set; }
- /// <summary>
- /// 备注
- /// </summary>
- [MaxLength(1000, ErrorMessage = "备注长度不能超过1000个字符")]
- [DisplayName("备注")]
- [AmisTextareaField(ColumnRatio = 12)]
- public string? Remarks { get; set; }
- /// <summary>
- /// 是否激活
- /// </summary>
- [DisplayName("是否激活")]
- [AmisFormField(ColumnRatio = 6)]
- public bool IsActive { get; set; }
- }
复制代码 2.4 EmployeeQueryDto(查询DTO)
- // Dtos/Employee/EmployeeQueryDto.cs
- using CodeSpirit.Amis.Attributes.FormFields;
- using CodeSpirit.Core.Dtos;
- using CodeSpirit.IdentityApi.Data.Models;
- using System.ComponentModel;
- namespace CodeSpirit.IdentityApi.Dtos.Employee;
- /// <summary>
- /// 职工查询数据传输对象
- /// </summary>
- public class EmployeeQueryDto : QueryDtoBase
- {
- /// <summary>
- /// 关键字搜索(姓名、工号、身份证、手机、邮箱)
- /// </summary>
- [DisplayName("关键字")]
- public string? Keywords { get; set; }
- /// <summary>
- /// 是否激活
- /// </summary>
- [DisplayName("是否激活")]
- public bool? IsActive { get; set; }
- /// <summary>
- /// 性别筛选
- /// </summary>
- [DisplayName("性别")]
- public Gender? Gender { get; set; }
- /// <summary>
- /// 部门ID筛选
- /// </summary>
- [DisplayName("部门")]
- [AmisInputTreeField(
- DataSource = "${ROOT_API}/api/identity/Departments/tree",
- Multiple = false,
- JoinValues = true,
- ExtractValue = false,
- ShowOutline = true,
- LabelField = "name",
- ValueField = "id",
- Required = false,
- Clearable = true,
- SubmitOnChange = true,
- HeightAuto = true,
- SelectFirst = false,
- InputOnly = true,
- ShowIcon = true
- )]
- [PageAside()]
- public long? DepartmentId { get; set; }
- /// <summary>
- /// 在职状态筛选
- /// </summary>
- [DisplayName("在职状态")]
- public EmploymentStatus? EmploymentStatus { get; set; }
- /// <summary>
- /// 入职日期范围
- /// </summary>
- [DisplayName("入职日期")]
- public DateTime[]? HireDate { get; set; }
- /// <summary>
- /// 职位
- /// </summary>
- [DisplayName("职位")]
- public string? Position { get; set; }
- /// <summary>
- /// 职级
- /// </summary>
- [DisplayName("职级")]
- public string? JobLevel { get; set; }
- }
复制代码 说明:
查询DTO特性:
- QueryDtoBase:基础查询DTO,提供了Page、PerPage、OrderBy、OrderDir、Keywords等分页和排序属性
- AmisInputTreeField:树形选择组件(用于查询表单)
- DataSource:数据源URL
- SubmitOnChange:选择后自动提交查询
- Searchable:是否可搜索
- Clearable:是否可清除
- ShowOutline:是否显示轮廓
- HeightAuto:高度自适应
- PageAside()特性:标记该字段在页面侧边栏显示
- 标记了此特性的字段会自动从主查询表单中排除,避免重复显示
- 特别适用于树形选择、分类筛选等需要独立展示的字段
- 侧边栏字段的变化会自动触发主内容区域的查询刷新(通过SubmitOnChange配置)
- 可以配置侧边栏的位置(左侧/右侧)、宽度、是否固定等属性
查询字段特性:
- 查询DTO中的字段可以使用表单特性(如AmisInputTreeField、AmisSelectField等)来配置查询表单的显示
- 支持多条件组合查询,提升查询灵活性
- 枚举类型字段会自动生成下拉选择组件
- 日期类型字段可以使用AmisDateFieldAttribute配置日期范围选择
3. 配置AutoMapper映射
在MappingProfiles目录下创建映射配置:- // MappingProfiles/EmployeeProfile.cs
- using AutoMapper;
- using CodeSpirit.IdentityApi.Data.Models;
- using CodeSpirit.IdentityApi.Dtos.Employee;
- using CodeSpirit.Shared.Extensions;
- namespace CodeSpirit.IdentityApi.MappingProfiles;
- /// <summary>
- /// 职工映射配置
- /// </summary>
- public class EmployeeProfile : Profile
- {
- /// <summary>
- /// 构造函数
- /// </summary>
- public EmployeeProfile()
- {
- // 使用扩展方法配置基本CRUD映射(自动处理Include导航属性)
- this.ConfigureBaseCRUDIMappings<
- Employee,
- EmployeeDto,
- long,
- CreateEmployeeDto,
- UpdateEmployeeDto,
- CreateEmployeeDto>();
-
- // 自定义映射:映射部门名称和用户名
- CreateMap<Employee, EmployeeDto>()
- .ForMember(dest => dest.DepartmentName, opt => opt.MapFrom(src => src.Department != null ? src.Department.Name : null))
- .ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.User != null ? src.User.UserName : null));
- }
- }
复制代码 说明:
- ConfigureBaseCRUDIMappings扩展方法自动配置基本的CRUD映射
- 使用ForMember自定义字段映射逻辑,将导航属性映射到DTO
- 支持多个DTO类型的映射配置
4. 创建服务接口和实现
4.1 服务接口
- // Services/IEmployeeService.cs
- using CodeSpirit.Core;
- using CodeSpirit.IdentityApi.Data.Models;
- using CodeSpirit.IdentityApi.Dtos.Employee;
- using CodeSpirit.Shared.Services;
- namespace CodeSpirit.IdentityApi.Services;
- /// <summary>
- /// 职工服务接口
- /// </summary>
- public interface IEmployeeService : IBaseCRUDIService<Employee, EmployeeDto, long, CreateEmployeeDto, UpdateEmployeeDto, EmployeeBatchImportItemDto>, IScopedDependency
- {
- /// <summary>
- /// 获取职工列表(分页)
- /// </summary>
- /// <param name="queryDto">查询条件</param>
- /// <returns>职工分页列表</returns>
- Task<PageList<EmployeeDto>> GetEmployeesAsync(EmployeeQueryDto queryDto);
- /// <summary>
- /// 根据部门获取职工列表
- /// </summary>
- /// <param name="departmentId">部门ID</param>
- /// <param name="includeSubDepartments">是否包含子部门</param>
- /// <returns>职工列表</returns>
- Task<List<EmployeeDto>> GetEmployeesByDepartmentAsync(long departmentId, bool includeSubDepartments = false);
- /// <summary>
- /// 设置职工激活状态
- /// </summary>
- /// <param name="id">职工ID</param>
- /// <param name="isActive">是否激活</param>
- Task SetActiveStatusAsync(long id, bool isActive);
- /// <summary>
- /// 转移职工到新部门
- /// </summary>
- /// <param name="employeeId">职工ID</param>
- /// <param name="newDepartmentId">新部门ID</param>
- Task TransferEmployeeAsync(long employeeId, long? newDepartmentId);
- /// <summary>
- /// 办理职工离职
- /// </summary>
- /// <param name="employeeId">职工ID</param>
- /// <param name="terminationDate">离职日期</param>
- Task TerminateEmployeeAsync(long employeeId, DateTime terminationDate);
- /// <summary>
- /// 验证工号是否唯一
- /// </summary>
- /// <param name="employeeNo">工号</param>
- /// <param name="excludeId">排除的职工ID(用于更新时验证)</param>
- /// <returns>是否唯一</returns>
- Task<bool> IsEmployeeNoUniqueAsync(string employeeNo, long? excludeId = null);
- }
复制代码 4.2 服务实现
- // Services/EmployeeService.cs
- using AutoMapper;
- using CodeSpirit.Core;
- using CodeSpirit.Core.IdGenerator;
- using CodeSpirit.IdentityApi.Data;
- using CodeSpirit.IdentityApi.Data.Models;
- using CodeSpirit.IdentityApi.Dtos.Employee;
- using CodeSpirit.IdentityApi.Utilities;
- using CodeSpirit.Shared.Repositories;
- using CodeSpirit.Shared.Services;
- using CodeSpirit.Shared.Dtos.Common;
- using LinqKit;
- using Microsoft.AspNetCore.Identity;
- using Microsoft.EntityFrameworkCore;
- namespace CodeSpirit.IdentityApi.Services;
- /// <summary>
- /// 职工服务实现
- /// </summary>
- public class EmployeeService : BaseCRUDIService<Employee, EmployeeDto, long, CreateEmployeeDto, UpdateEmployeeDto, EmployeeBatchImportItemDto>, IEmployeeService
- {
- private readonly IRepository<Employee> _employeeRepository;
- private readonly IRepository<Department> _departmentRepository;
- private readonly IRepository _userRepository;
- private readonly ILogger<EmployeeService> _logger;
- private readonly IIdGenerator _idGenerator;
- private readonly ICurrentUser _currentUser;
- private readonly ApplicationDbContext _dbContext;
- private readonly IDepartmentService _departmentService;
- private readonly UserManager _userManager;
- /// <summary>
- /// 构造函数
- /// </summary>
- public EmployeeService(
- IRepository<Employee> employeeRepository,
- IRepository<Department> departmentRepository,
- IRepository userRepository,
- IMapper mapper,
- ILogger<EmployeeService> logger,
- IIdGenerator idGenerator,
- ICurrentUser currentUser,
- ApplicationDbContext dbContext,
- IDepartmentService departmentService,
- UserManager userManager,
- EnhancedBatchImportHelper<EmployeeBatchImportItemDto> importHelper)
- : base(employeeRepository, mapper, importHelper)
- {
- _employeeRepository = employeeRepository;
- _departmentRepository = departmentRepository;
- _userRepository = userRepository;
- _logger = logger;
- _idGenerator = idGenerator;
- _currentUser = currentUser;
- _dbContext = dbContext;
- _departmentService = departmentService;
- _userManager = userManager;
- }
- /// <summary>
- /// 获取职工列表(分页)
- /// </summary>
- public async Task<PageList<EmployeeDto>> GetEmployeesAsync(EmployeeQueryDto queryDto)
- {
- var predicate = PredicateBuilder.New<Employee>(true);
- // 应用搜索关键词过滤
- if (!string.IsNullOrWhiteSpace(queryDto.Keywords))
- {
- string searchLower = queryDto.Keywords.ToLower();
- predicate = predicate.Or(e => e.Name.ToLower().Contains(searchLower));
- predicate = predicate.Or(e => e.EmployeeNo.ToLower().Contains(searchLower));
- predicate = predicate.Or(e => e.IdNo.Contains(queryDto.Keywords));
- predicate = predicate.Or(e => e.PhoneNumber.Contains(queryDto.Keywords));
- predicate = predicate.Or(e => e.Email.ToLower().Contains(searchLower));
- }
- // 应用其他过滤条件
- if (queryDto.IsActive.HasValue)
- {
- predicate = predicate.And(e => e.IsActive == queryDto.IsActive.Value);
- }
- if (queryDto.Gender.HasValue)
- {
- predicate = predicate.And(e => e.Gender == queryDto.Gender.Value);
- }
- if (queryDto.DepartmentId.HasValue)
- {
- predicate = predicate.And(e => e.DepartmentId == queryDto.DepartmentId.Value);
- }
- if (queryDto.EmploymentStatus.HasValue)
- {
- predicate = predicate.And(e => e.EmploymentStatus == queryDto.EmploymentStatus.Value);
- }
- if (!string.IsNullOrWhiteSpace(queryDto.Position))
- {
- predicate = predicate.And(e => e.Position == queryDto.Position);
- }
- if (!string.IsNullOrWhiteSpace(queryDto.JobLevel))
- {
- predicate = predicate.And(e => e.JobLevel == queryDto.JobLevel);
- }
- if (queryDto.HireDate != null && queryDto.HireDate.Length == 2)
- {
- predicate = predicate.And(e => e.HireDate >= queryDto.HireDate[0]);
- predicate = predicate.And(e => e.HireDate <= queryDto.HireDate[1]);
- }
- // 创建查询
- var query = _employeeRepository.CreateQuery()
- .Include(e => e.Department)
- .Include(e => e.User)
- .Where(predicate);
- // 执行分页查询
- var totalCount = await query.CountAsync();
- var employees = await query
- .OrderByDescending(e => e.CreatedAt)
- .Skip((queryDto.Page - 1) * queryDto.PerPage)
- .Take(queryDto.PerPage)
- .ToListAsync();
- // 映射到DTO
- var employeeDtos = Mapper.Map<List<EmployeeDto>>(employees);
- // 设置关联数据
- foreach (var dto in employeeDtos)
- {
- var employee = employees.First(e => e.Id == dto.Id);
- dto.DepartmentName = employee.Department?.Name;
- dto.UserName = employee.User?.UserName;
- }
- return new PageList<EmployeeDto>(employeeDtos, totalCount);
- }
- /// <summary>
- /// 根据部门获取职工列表
- /// </summary>
- public async Task<List<EmployeeDto>> GetEmployeesByDepartmentAsync(long departmentId, bool includeSubDepartments = false)
- {
- var departmentIds = new List<long> { departmentId };
-
- if (includeSubDepartments)
- {
- var subDepartments = await _departmentService.GetSubDepartmentsAsync(departmentId);
- departmentIds.AddRange(subDepartments.Select(d => d.Id));
- }
- var employees = await _employeeRepository.CreateQuery()
- .Include(e => e.Department)
- .Include(e => e.User)
- .Where(e => departmentIds.Contains(e.DepartmentId ?? 0))
- .ToListAsync();
- return Mapper.Map<List<EmployeeDto>>(employees);
- }
- /// <summary>
- /// 设置职工激活状态
- /// </summary>
- public async Task SetActiveStatusAsync(long id, bool isActive)
- {
- var employee = await _employeeRepository.GetByIdAsync(id);
- if (employee == null)
- {
- throw new AppServiceException(404, "职工不存在");
- }
- employee.IsActive = isActive;
- await _employeeRepository.UpdateAsync(employee);
- }
- /// <summary>
- /// 转移职工到新部门
- /// </summary>
- public async Task TransferEmployeeAsync(long employeeId, long? newDepartmentId)
- {
- var employee = await _employeeRepository.GetByIdAsync(employeeId);
- if (employee == null)
- {
- throw new AppServiceException(404, "职工不存在");
- }
- if (newDepartmentId.HasValue)
- {
- var departmentExists = await _departmentRepository.ExistsAsync(d => d.Id == newDepartmentId.Value);
- if (!departmentExists)
- {
- throw new AppServiceException(400, "部门不存在");
- }
- }
- employee.DepartmentId = newDepartmentId;
- await _employeeRepository.UpdateAsync(employee);
- }
- /// <summary>
- /// 办理职工离职
- /// </summary>
- public async Task TerminateEmployeeAsync(long employeeId, DateTime terminationDate)
- {
- var employee = await _employeeRepository.GetByIdAsync(employeeId);
- if (employee == null)
- {
- throw new AppServiceException(404, "职工不存在");
- }
- employee.EmploymentStatus = EmploymentStatus.Resigned;
- employee.TerminationDate = terminationDate;
- employee.IsActive = false;
-
- await _employeeRepository.UpdateAsync(employee);
- }
- /// <summary>
- /// 验证工号是否唯一
- /// </summary>
- public async Task<bool> IsEmployeeNoUniqueAsync(string employeeNo, long? excludeId = null)
- {
- var query = _employeeRepository.CreateQuery()
- .Where(e => e.EmployeeNo == employeeNo && e.TenantId == _currentUser.TenantId);
- if (excludeId.HasValue)
- {
- query = query.Where(e => e.Id != excludeId.Value);
- }
- return !await query.AnyAsync();
- }
- /// <summary>
- /// 验证创建DTO
- /// </summary>
- protected override async Task ValidateCreateDto(CreateEmployeeDto createDto)
- {
- await base.ValidateCreateDto(createDto);
- // 验证工号唯一性
- bool isUnique = await IsEmployeeNoUniqueAsync(createDto.EmployeeNo);
- if (!isUnique)
- {
- throw new AppServiceException(400, $"工号 {createDto.EmployeeNo} 已存在,请使用其他工号");
- }
- // 验证部门是否存在
- if (createDto.DepartmentId.HasValue)
- {
- var departmentExists = await _departmentRepository.ExistsAsync(d => d.Id == createDto.DepartmentId.Value);
- if (!departmentExists)
- {
- throw new AppServiceException(400, "部门不存在");
- }
- }
- // 验证用户是否存在(如果指定了用户ID)
- if (createDto.UserId.HasValue)
- {
- var userExists = await _userRepository.ExistsAsync(u => u.Id == createDto.UserId.Value);
- if (!userExists)
- {
- throw new AppServiceException(400, "用户不存在");
- }
- }
- }
- /// <summary>
- /// 验证更新DTO
- /// </summary>
- protected override async Task ValidateUpdateDto(long id, UpdateEmployeeDto updateDto)
- {
- await base.ValidateUpdateDto(id, updateDto);
- // 验证工号唯一性(排除当前记录)
- bool isUnique = await IsEmployeeNoUniqueAsync(updateDto.EmployeeNo, id);
- if (!isUnique)
- {
- throw new AppServiceException(400, $"工号 {updateDto.EmployeeNo} 已存在,请使用其他工号");
- }
- // 验证部门是否存在
- if (updateDto.DepartmentId.HasValue)
- {
- var departmentExists = await _departmentRepository.ExistsAsync(d => d.Id == updateDto.DepartmentId.Value);
- if (!departmentExists)
- {
- throw new AppServiceException(400, "部门不存在");
- }
- }
- // 验证用户是否存在(如果指定了用户ID)
- if (updateDto.UserId.HasValue)
- {
- var userExists = await _userRepository.ExistsAsync(u => u.Id == updateDto.UserId.Value);
- if (!userExists)
- {
- throw new AppServiceException(400, "用户不存在");
- }
- }
- }
- /// <summary>
- /// 创建实体前的处理
- /// </summary>
- protected override async Task<Employee> OnCreating(CreateEmployeeDto createDto)
- {
- var employee = await base.OnCreating(createDto);
-
- // 设置租户ID
- employee.TenantId = _currentUser.TenantId;
-
- // 生成ID(如果需要)
- if (employee.Id == 0)
- {
- employee.Id = await _idGenerator.GenerateIdAsync();
- }
- return employee;
- }
- }
复制代码 说明:
- 继承自BaseCRUDIService,自动获得标准的CRUD方法和批量导入功能
- 实现IScopedDependency接口,服务会自动注册
- 重写ValidateCreateDto和ValidateUpdateDto方法实现业务验证(工号唯一性、部门存在性等)
- 重写OnCreating方法设置租户ID和生成ID
- 使用LinqKit的PredicateBuilder构建动态查询条件
- 提供额外的业务方法(设置激活状态、转移部门、办理离职等)
5. 创建控制器
在Controllers目录下创建控制器:- // Controllers/EmployeesController.cs
- using CodeSpirit.Core;
- using CodeSpirit.Core.Attributes;
- using CodeSpirit.Core.Dtos;
- using CodeSpirit.Core.Enums;
- using CodeSpirit.IdentityApi.Dtos.Employee;
- using CodeSpirit.IdentityApi.Services;
- using CodeSpirit.Shared.Dtos.Common;
- using Microsoft.AspNetCore.Mvc;
- using System.ComponentModel;
- namespace CodeSpirit.IdentityApi.Controllers;
- /// <summary>
- /// 职工管理控制器
- /// </summary>
- [DisplayName("职工管理")]
- [Navigation(Icon = "fa-solid fa-user-tie", PlatformType = PlatformType.Tenant)]
- public class EmployeesController : ApiControllerBase
- {
- private readonly IEmployeeService _employeeService;
- /// <summary>
- /// 构造函数
- /// </summary>
- public EmployeesController(IEmployeeService employeeService)
- {
- _employeeService = employeeService;
- }
- /// <summary>
- /// 获取职工列表
- /// </summary>
- /// <param name="queryDto">查询条件</param>
- /// <returns>职工列表结果</returns>
- [HttpGet]
- [DisplayName("获取职工列表")]
- public async Task>>> GetEmployees([FromQuery] EmployeeQueryDto queryDto)
- {
- var employees = await _employeeService.GetEmployeesAsync(queryDto);
- return SuccessResponse(employees);
- }
- /// <summary>
- /// 根据部门获取职工列表
- /// </summary>
- /// <param name="departmentId">部门ID</param>
- /// <param name="includeSubDepartments">是否包含子部门</param>
- /// <returns>职工列表</returns>
- [HttpGet("department/{departmentId}")]
- [DisplayName("根据部门获取职工")]
- public async Task>>> GetEmployeesByDepartment(
- long departmentId,
- [FromQuery] bool includeSubDepartments = false)
- {
- var employees = await _employeeService.GetEmployeesByDepartmentAsync(departmentId, includeSubDepartments);
- return SuccessResponse(employees);
- }
- /// <summary>
- /// 获取职工详情
- /// </summary>
- /// <param name="id">职工ID</param>
- /// <returns>职工详细信息</returns>
- [HttpGet("{id:long}")]
- [DisplayName("获取职工详情")]
- public async Task>> GetEmployee(long id)
- {
- var employee = await _employeeService.GetAsync(id);
- return SuccessResponse(employee);
- }
- /// <summary>
- /// 创建职工
- /// </summary>
- /// <param name="createDto">创建职工请求数据</param>
- /// <returns>创建的职工信息</returns>
- [HttpPost]
- [DisplayName("创建职工")]
- public async Task>> CreateEmployee(CreateEmployeeDto createDto)
- {
- ArgumentNullException.ThrowIfNull(createDto);
- var employeeDto = await _employeeService.CreateAsync(createDto);
- return SuccessResponse(employeeDto);
- }
- /// <summary>
- /// 更新职工
- /// </summary>
- /// <param name="id">职工ID</param>
- /// <param name="updateDto">更新职工请求数据</param>
- /// <returns>更新操作结果</returns>
- [HttpPut("{id:long}")]
- [DisplayName("更新职工")]
- public async Task> UpdateEmployee(long id, UpdateEmployeeDto updateDto)
- {
- await _employeeService.UpdateAsync(id, updateDto);
- return SuccessResponse();
- }
- /// <summary>
- /// 删除职工
- /// </summary>
- /// <param name="id">职工ID</param>
- /// <returns>删除操作结果</returns>
- [HttpDelete("{id:long}")]
- [Operation("删除", "ajax", null, "确定要删除此职工吗?")]
- [DisplayName("删除职工")]
- public async Task> DeleteEmployee(long id)
- {
- await _employeeService.DeleteAsync(id);
- return SuccessResponse();
- }
- /// <summary>
- /// 批量删除职工
- /// </summary>
- /// <param name="request">批量删除请求</param>
- /// <returns>批量删除操作结果</returns>
- [HttpPost("batch-delete")]
- [Operation("批量删除", "ajax", null, "确定要批量删除选中的职工吗?", isBulkOperation: true)]
- [DisplayName("批量删除职工")]
- public async Task> BatchDeleteEmployees([FromBody] BatchOperationDto<long> request)
- {
- ArgumentNullException.ThrowIfNull(request);
- (int successCount, List<long> failedIds) = await _employeeService.BatchDeleteAsync(request.Ids);
-
- return failedIds.Any()
- ? SuccessResponse($"成功删除 {successCount} 个职工,但以下职工删除失败: {string.Join(", ", failedIds)}")
- : SuccessResponse($"成功删除 {successCount} 个职工!");
- }
- /// <summary>
- /// 设置职工激活状态
- /// </summary>
- /// <param name="id">职工ID</param>
- /// <param name="isActive">是否激活</param>
- /// <returns>操作结果</returns>
- [HttpPut("{id:long}/active")]
- [DisplayName("设置激活状态")]
- public async Task> SetActiveStatus(long id, [FromBody] bool isActive)
- {
- await _employeeService.SetActiveStatusAsync(id, isActive);
- return SuccessResponse();
- }
- /// <summary>
- /// 转移职工到新部门
- /// </summary>
- /// <param name="id">职工ID</param>
- /// <param name="request">转移请求</param>
- /// <returns>操作结果</returns>
- [HttpPut("{id:long}/transfer")]
- [DisplayName("转移部门")]
- public async Task> TransferEmployee(long id, [FromBody] TransferEmployeeRequest request)
- {
- await _employeeService.TransferEmployeeAsync(id, request.DepartmentId);
- return SuccessResponse();
- }
- /// <summary>
- /// 办理职工离职
- /// </summary>
- /// <param name="id">职工ID</param>
- /// <param name="request">离职请求</param>
- /// <returns>操作结果</returns>
- [HttpPut("{id:long}/terminate")]
- [DisplayName("办理离职")]
- public async Task> TerminateEmployee(long id, [FromBody] TerminateEmployeeRequest request)
- {
- await _employeeService.TerminateEmployeeAsync(id, request.TerminationDate);
- return SuccessResponse();
- }
- }
- /// <summary>
- /// 转移职工请求
- /// </summary>
- public class TransferEmployeeRequest
- {
- public long? DepartmentId { get; set; }
- }
- /// <summary>
- /// 离职请求
- /// </summary>
- public class TerminateEmployeeRequest
- {
- public DateTime TerminationDate { get; set; }
- }
复制代码 说明:
- 继承自ApiControllerBase,自动获得统一的响应格式和异常处理
- DisplayName特性用于前端界面显示
- Navigation特性用于添加到导航菜单
- Operation特性用于配置操作按钮(删除确认对话框)
- 使用SuccessResponse方法返回统一的成功响应
- 提供额外的业务操作接口(设置激活状态、转移部门、办理离职等)
6. 配置数据库上下文
在Data目录下的DbContext中添加实体:- // Data/ApplicationDbContext.cs
- using CodeSpirit.IdentityApi.Data.Models;
- using CodeSpirit.Shared.Data;
- using Microsoft.EntityFrameworkCore;
- namespace CodeSpirit.IdentityApi.Data;
- /// <summary>
- /// 身份认证系统数据库上下文 - 支持多租户和多数据库
- /// </summary>
- public class ApplicationDbContext : MultiDatabaseDbContextBase
- {
- /// <summary>
- /// 职工
- /// </summary>
- public DbSet<Employee> Employees => Set<Employee>();
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- base.OnModelCreating(modelBuilder);
- // 配置Employee实体
- modelBuilder.Entity<Employee>(entity =>
- {
- entity.ToTable(nameof(Employee));
- entity.Property(e => e.Id).ValueGeneratedNever();
- // 租户感知的工号复合唯一索引:同一租户内工号唯一
- entity.HasIndex(e => new { e.TenantId, e.EmployeeNo })
- .IsUnique()
- .HasDatabaseName("IX_Employee_TenantId_EmployeeNo");
- // 索引 DepartmentId,提高查询部门员工的性能
- entity.HasIndex(e => e.DepartmentId)
- .HasDatabaseName("IX_Employee_DepartmentId");
- // 索引 UserId,提高查询用户关联的性能
- entity.HasIndex(e => e.UserId)
- .HasDatabaseName("IX_Employee_UserId");
- // 索引 IsActive,提高按状态过滤的性能
- entity.HasIndex(e => e.IsActive)
- .HasDatabaseName("IX_Employee_IsActive");
- // 索引 EmploymentStatus,提高按在职状态过滤的性能
- entity.HasIndex(e => e.EmploymentStatus)
- .HasDatabaseName("IX_Employee_EmploymentStatus");
- // 配置与部门的关系
- entity.HasOne(e => e.Department)
- .WithMany()
- .HasForeignKey(e => e.DepartmentId)
- .OnDelete(DeleteBehavior.SetNull);
- // 配置与用户的关系
- entity.HasOne(e => e.User)
- .WithMany()
- .HasForeignKey(e => e.UserId)
- .OnDelete(DeleteBehavior.SetNull);
- });
- }
- }
复制代码 说明:
- 继承自MultiDatabaseDbContextBase,支持MySQL和SQL Server
- 配置表名、主键、字段长度等
- 配置复合唯一索引(租户ID + 工号),确保同一租户内工号唯一
- 配置关联关系的级联删除策略(SetNull表示删除部门或用户时,职工记录保留但关联字段设为null)
- 添加必要的索引提升查询性能
7. 服务注册
CodeSpirit框架通过标记接口自动注册服务,无需手动注册:- // IEmployeeService接口继承了IScopedDependency接口
- public interface IEmployeeService : IBaseCRUDIService<...>, IScopedDependency
- {
- // ...
- }
复制代码 说明:
- 服务接口继承IScopedDependency接口,服务会自动注册为Scoped生命周期
- 框架会自动扫描并注册所有标记接口的服务
- 无需在Program.cs中手动注册
8. 创建数据库迁移
CodeSpirit框架支持多数据库架构,迁移文件按数据库类型分离存储。创建迁移时必须指定迁移目录参数。- # 进入IdentityApi项目目录
- cd Src/ApiServices/CodeSpirit.IdentityApi
- # 创建迁移(根据数据库类型选择)
- # MySQL - 迁移文件将保存到 Migrations/MySql/ 目录
- dotnet ef migrations add AddEmployees --context MySqlApplicationDbContext --output-dir Migrations/MySql
- # SQL Server - 迁移文件将保存到 Migrations/SqlServer/ 目录
- dotnet ef migrations add AddEmployees --context SqlServerApplicationDbContext --output-dir Migrations/SqlServer
- # 应用迁移
- dotnet ef database update --context MySqlApplicationDbContext
- # 或
- dotnet ef database update --context SqlServerApplicationDbContext
复制代码 迁移目录结构:- Src/ApiServices/CodeSpirit.IdentityApi/
- ├── Migrations/
- │ ├── MySql/ # MySQL迁移文件
- │ │ ├── 20251222_AddEmployees.cs
- │ │ ├── 20251222_AddEmployees.Designer.cs
- │ │ └── MySqlApplicationDbContextModelSnapshot.cs
- │ └── SqlServer/ # SQL Server迁移文件
- │ ├── 20251222_AddEmployees.cs
- │ ├── 20251222_AddEmployees.Designer.cs
- │ └── SqlServerApplicationDbContextModelSnapshot.cs
复制代码 说明:
- --output-dir参数用于指定迁移文件的输出目录
- MySQL迁移文件必须保存到Migrations/MySql/目录
- SQL Server迁移文件必须保存到Migrations/SqlServer/目录
- 每个数据库类型都有独立的ModelSnapshot.cs文件
- 这样可以确保不同数据库类型的迁移文件互不干扰
功能特性
通过以上步骤,您已经完成了一个完整的CRUD功能开发。CodeSpirit框架会自动提供以下功能:
自动生成的功能
- ✅ AMIS前端界面:基于控制器和DTO的特性自动生成
- 表格展示(支持头像、日期格式化、状态显示等)
- 表单编辑(支持表单分组、树形选择、图片上传等)
- 多条件搜索筛选(关键字、部门、状态、日期范围等)
- 批量操作(批量删除等)
- ✅ 统一的API响应格式:使用ApiResponse统一响应
- ✅ 分页查询:支持分页、排序、多条件筛选
- ✅ 批量操作:支持批量删除、批量导入等操作
- ✅ 异常处理:统一的异常处理和错误响应
- ✅ 权限控制:支持基于特性的权限控制
- ✅ 审计日志:自动记录创建、更新、删除操作
- ✅ 多租户支持:自动进行数据隔离
- ✅ 软删除支持:删除操作使用软删除,数据可恢复
标准CRUD操作
操作HTTP方法路径说明查询列表GET/api/identity/Employees支持多条件查询和关键字搜索查询详情GET/api/identity/Employees/{id}根据ID获取单个职工创建POST/api/identity/Employees创建新职工更新PUT/api/identity/Employees/{id}更新职工信息删除DELETE/api/identity/Employees/{id}删除单个职工(软删除)批量删除POST/api/identity/Employees/batch-delete批量删除职工根据部门查询GET/api/identity/Employees/department/{departmentId}根据部门获取职工列表设置激活状态PUT/api/identity/Employees/{id}/active设置职工激活状态转移部门PUT/api/identity/Employees/{id}/transfer转移职工到新部门办理离职PUT/api/identity/Employees/{id}/terminate办理职工离职业务验证示例
创建时验证
- protected override async Task ValidateCreateDto(CreateEmployeeDto createDto)
- {
- await base.ValidateCreateDto(createDto);
- // 验证工号唯一性
- bool isUnique = await IsEmployeeNoUniqueAsync(createDto.EmployeeNo);
- if (!isUnique)
- {
- throw new AppServiceException(400, $"工号 {createDto.EmployeeNo} 已存在,请使用其他工号");
- }
- // 验证部门是否存在
- if (createDto.DepartmentId.HasValue)
- {
- var departmentExists = await _departmentRepository.ExistsAsync(d => d.Id == createDto.DepartmentId.Value);
- if (!departmentExists)
- {
- throw new AppServiceException(400, "部门不存在");
- }
- }
- // 验证用户是否存在(如果指定了用户ID)
- if (createDto.UserId.HasValue)
- {
- var userExists = await _userRepository.ExistsAsync(u => u.Id == createDto.UserId.Value);
- if (!userExists)
- {
- throw new AppServiceException(400, "用户不存在");
- }
- }
- }
复制代码 更新时验证
- protected override async Task ValidateUpdateDto(long id, UpdateEmployeeDto updateDto)
- {
- await base.ValidateUpdateDto(id, updateDto);
- // 验证工号唯一性(排除当前记录)
- bool isUnique = await IsEmployeeNoUniqueAsync(updateDto.EmployeeNo, id);
- if (!isUnique)
- {
- throw new AppServiceException(400, $"工号 {updateDto.EmployeeNo} 已存在,请使用其他工号");
- }
- // 验证部门是否存在
- if (updateDto.DepartmentId.HasValue)
- {
- var departmentExists = await _departmentRepository.ExistsAsync(d => d.Id == updateDto.DepartmentId.Value);
- if (!departmentExists)
- {
- throw new AppServiceException(400, "部门不存在");
- }
- }
- }
复制代码 创建前处理
- protected override async Task<Employee> OnCreating(CreateEmployeeDto createDto)
- {
- var employee = await base.OnCreating(createDto);
-
- // 设置租户ID
- employee.TenantId = _currentUser.TenantId;
-
- // 生成ID(如果需要)
- if (employee.Id == 0)
- {
- employee.Id = await _idGenerator.GenerateIdAsync();
- }
- return employee;
- }
复制代码 扩展功能示例
添加权限控制
- [HttpPost]
- [DisplayName("创建职工")]
- [Permission("identity_employees_create")] // 添加权限控制
- public async Task>> CreateEmployee(CreateEmployeeDto createDto)
- {
- // ...
- }
复制代码 添加导航菜单
- [DisplayName("职工管理")]
- [Navigation(Icon = "fa-solid fa-user-tie", PlatformType = PlatformType.Tenant)] // 添加到导航菜单
- public class EmployeesController : ApiControllerBase
- {
- // ...
- }
复制代码 自定义查询方法
- /// <summary>
- /// 获取在职职工列表
- /// </summary>
- public async Task<List<EmployeeDto>> GetActiveEmployeesAsync()
- {
- var employees = await Repository.CreateQuery()
- .Where(e => e.IsActive && e.EmploymentStatus == EmploymentStatus.Active)
- .Include(e => e.Department)
- .Include(e => e.User)
- .ToListAsync();
- return Mapper.Map<List<EmployeeDto>>(employees);
- }
复制代码 使用PageAside特性实现侧边栏筛选
PageAside()特性用于将查询字段放置在页面侧边栏,特别适用于树形选择、分类筛选等场景。使用此特性后,该字段会从主查询表单中移除,仅在侧边栏显示。
特性说明:- /// <summary>
- /// 部门ID筛选
- /// </summary>
- [DisplayName("部门")]
- [AmisInputTreeField(
- DataSource = "${ROOT_API}/api/identity/Departments/tree",
- Multiple = false,
- JoinValues = true,
- ExtractValue = false,
- ShowOutline = true,
- LabelField = "name",
- ValueField = "id",
- Required = false,
- Clearable = true,
- SubmitOnChange = true, // 选择后自动提交查询
- HeightAuto = true,
- SelectFirst = false,
- InputOnly = true,
- ShowIcon = true
- )]
- [PageAside()] // 标记为侧边栏字段
- public long? DepartmentId { get; set; }
复制代码
PageAside特性的主要属性:
- Target:表单提交目标,如果为空则自动设置为CRUD组件名称
- SubmitOnInit:是否在初始化时提交,默认为false
- WrapWithPanel:是否不使用面板包装,默认为false
- AsideResizor:侧边栏宽度是否可调整,默认为true
- AsideMinWidth:侧边栏最小宽度(像素),默认为0
- AsideMaxWidth:侧边栏最大宽度(像素),默认为0
- AsideSticky:侧边栏是否固定,默认为true
- AsidePosition:侧边栏位置(Left/Right),默认为Left
使用场景:
- 树形分类筛选:如部门树、分类树等,放在侧边栏作为导航筛选器
- 独立筛选器:需要独立展示的筛选条件,避免主表单过于拥挤
- 联动查询:侧边栏字段变化时自动触发主内容区域刷新
注意事项:
- 标记了PageAside()特性的字段会自动从主查询表单中排除
- 建议配合SubmitOnChange = true使用,实现选择后自动查询
- 侧边栏字段的查询条件会自动合并到主查询中
最佳实践
- 实体设计:
- 实现IFullAuditable接口获得完整的审计字段(创建、更新、删除)
- 实现IMultiTenant接口支持多租户数据隔离
- 实现IIsActive接口支持激活状态管理
- 合理设计导航属性,使用Include避免N+1查询问题
- 为唯一性字段创建复合唯一索引(租户ID + 业务字段)
- DTO分离:
- 为创建、更新、查询分别创建DTO
- 使用DisplayName特性提供友好的字段名称
- 使用AmisColumn特性控制前端表格列显示
- 使用FormGroup特性将表单字段分组,提升用户体验
- 使用AmisInputTreeField等特性自动生成合适的表单组件
- 服务层:
- 继承BaseCRUDIService获得CRUD和批量导入功能
- 服务接口继承IScopedDependency接口自动注册
- 重写ValidateCreateDto和ValidateUpdateDto实现业务验证
- 重写OnCreating方法设置租户ID和生成ID
- 使用LinqKit的PredicateBuilder构建动态查询条件
- 控制器:
- 保持简洁,主要调用服务层方法
- 使用DisplayName和Navigation特性
- 使用Operation特性配置操作按钮(删除确认对话框)
- 提供额外的业务操作接口(如设置激活状态、转移部门等)
- 验证:
- 使用DataAnnotations进行基础数据验证
- 重写服务层的验证方法实现业务验证(唯一性、关联存在性等)
- 使用AppServiceException抛出业务异常
- 在数据库层面创建唯一索引确保数据完整性
- 数据库设计:
- 为常用查询字段创建索引提升性能
- 合理配置关联关系的级联删除策略
- 使用复合唯一索引确保租户内业务字段唯一性
- 文档注释:
相关文档
- CodeSpirit.Core核心框架
- 开发环境搭建指南
- 项目整体架构设计
- 统一异常处理指南
总结
通过CodeSpirit框架的BaseCRUDIService和标准开发模式,您可以快速开发出功能完整的CRUD接口。职工管理模块展示了:
- ✅ 标准CRUD操作的实现
- ✅ 关联关系管理(部门、用户账号)
- ✅ 业务验证逻辑的编写(工号唯一性、部门存在性等)
- ✅ 多条件查询的实现(关键字、部门、状态、日期范围等)
- ✅ 表单分组展示的使用
- ✅ 额外业务操作的实现(设置激活状态、转移部门、办理离职等)
- ✅ AMIS特性的使用(表格列、表单字段、图片上传等)
框架会自动处理大部分样板代码,让您专注于业务逻辑的实现。
更多交流请关注“CodeSpirit-码灵”公众号进群!!!
祝您开发愉快!
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |