CodeSpirit CRUD开发完整指南
概述
本文档通过职工管理(Employee)的实际代码示例,展示如何使用CodeSpirit框架快速开发CRUD功能。该示例来自身份认证系统(IdentityApi),是一个标准的关联型CRUD模块,包含完整的验证逻辑、业务处理和关联关系管理。
最后更新: 2025年12月22日
框架版本: v2.0.0
示例来源: CodeSpirit.IdentityApi - 职工管理模块

开发流程概览
示例模块说明
职工管理(Employee)是一个典型的关联型CRUD模块,具有以下特点:
- ✅ 关联关系管理(部门、用户账号)
- ✅ 完整的CRUD操作
- ✅ 业务验证(工号唯一性、部门存在性、身份证格式等)
- ✅ 多条件查询(关键字、部门、状态、日期范围等)
- ✅ 表单分组展示(基本信息、联系方式、工作信息等)
- ✅ 多租户支持
- ✅ 审计字段自动记录
- ✅ 软删除支持
1. 创建实体模型
在Data/Models目录下创建实体类:
// Data/Models/Employee.cs
using CodeSpirit.Shared.Entities.Interfaces;
using CodeSpirit.MultiTenant.Abstractions;
using System.ComponentModel.DataAnnotations;
namespace CodeSpirit.IdentityApi.Data.Models;
/// <summary>
/// 职工信息
/// </summary>
public class Employee : IFullAuditable, IMultiTenant, IIsActive
{
/// <summary>
/// 职工ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 租户ID(多租户支持)
/// </summary>
[Required]
[MaxLength(50)]
public string TenantId { get; set; } = string.Empty;
/// <summary>
/// 工号(租户内唯一)
/// </summary>
[Required]
[MaxLength(50)]
public string EmployeeNo { get; set; } = string.Empty;
/// <summary>
/// 姓名
/// </summary>
[Required]
[MaxLength(100)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 性别
/// </summary>
public Gender Gender { get; set; }
/// <summary>
/// 身份证号码
/// </summary>
[MaxLength(18)]
public string? IdNo { get; set; }
/// <summary>
/// 出生日期
/// </summary>
public DateTime? BirthDate { get; set; }
/// <summary>
/// 手机号码
/// </summary>
[MaxLength(15)]
public string? PhoneNumber { get; set; }
/// <summary>
/// 电子邮箱
/// </summary>
[MaxLength(100)]
[EmailAddress]
public string? Email { get; set; }
/// <summary>
/// 部门ID
/// </summary>
public long? DepartmentId { get; set; }
/// <summary>
/// 所属部门(导航属性)
/// </summary>
public Department? Department { get; set; }
/// <summary>
/// 职位
/// </summary>
[MaxLength(100)]
public string? Position { get; set; }
/// <summary>
/// 职级
/// </summary>
[MaxLength(50)]
public string? JobLevel { get; set; }
/// <summary>
/// 入职日期
/// </summary>
public DateTime? HireDate { get; set; }
/// <summary>
/// 离职日期
/// </summary>
public DateTime? TerminationDate { get; set; }
/// <summary>
/// 在职状态
/// </summary>
public EmploymentStatus EmploymentStatus { get; set; }
/// <summary>
/// 关联的用户ID
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// 关联的用户账号(导航属性)
/// </summary>
public ApplicationUser? User { get; set; }
/// <summary>
/// 紧急联系人
/// </summary>
[MaxLength(100)]
public string? EmergencyContact { get; set; }
/// <summary>
/// 紧急联系电话
/// </summary>
[MaxLength(15)]
public string? EmergencyPhone { get; set; }
/// <summary>
/// 地址
/// </summary>
[MaxLength(500)]
public string? Address { get; set; }
/// <summary>
/// 备注
/// </summary>
[MaxLength(1000)]
public string? Remarks { get; set; }
/// <summary>
/// 是否激活
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// 头像地址
/// </summary>
[MaxLength(255)]
[DataType(DataType.ImageUrl)]
public string? AvatarUrl { get; set; }
// 审计字段(实现IFullAuditable接口)
public long CreatedBy { get; set; }
public DateTime CreatedAt { get; set; }
public long? UpdatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public long? DeletedBy { get; set; }
public DateTime? DeletedAt { get; set; }
public bool IsDeleted { get; set; }
}
说明:
- 实现
IFullAuditable接口,自动包含完整的审计字段(创建、更新、删除) - 实现
IMultiTenant接口,支持多租户数据隔离 - 实现
IIsActive接口,支持激活状态管理 - 使用
long作为主键类型 - 包含关联关系的导航属性(部门、用户账号)
- 支持软删除(
IsDeleted字段)
2. 创建DTO类
在Dtos/Employee目录下创建DTO类:
2.1 EmployeeDto(展示DTO)
// Dtos/Employee/EmployeeDto.cs
using CodeSpirit.Amis.Attributes.Columns;
using CodeSpirit.Core.Attributes;
using CodeSpirit.IdentityApi.Data.Models;
using System.ComponentModel;
namespace CodeSpirit.IdentityApi.Dtos.Employee;
/// <summary>
/// 职工数据传输对象
/// </summary>
public class EmployeeDto
{
/// <summary>
/// 职工ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 工号
/// </summary>
[DisplayName("工号")]
public string EmployeeNo { get; set; } = string.Empty;
/// <summary>
/// 姓名
/// </summary>
[DisplayName("姓名")]
[TplColumn(template: "${name}")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 头像地址
/// </summary>
[DisplayName("头像")]
[AvatarColumn(Text = "${name}", Src = "${avatarUrl}")]
[Badge(Animation = true, VisibleOn = "isActive", Level = "info")]
public string? AvatarUrl { get; set; }
/// <summary>
/// 性别
/// </summary>
[DisplayName("性别")]
public Gender Gender { get; set; }
/// <summary>
/// 手机号码
/// </summary>
[DisplayName("手机号码")]
public string? PhoneNumber { get; set; }
/// <summary>
/// 电子邮箱
/// </summary>
[DisplayName("电子邮箱")]
public string? Email { get; set; }
/// <summary>
/// 部门ID
/// </summary>
[AmisColumn(Hidden = true)]
public long? DepartmentId { get; set; }
/// <summary>
/// 部门名称
/// </summary>
[DisplayName("部门")]
public string? DepartmentName { get; set; }
/// <summary>
/// 职位
/// </summary>
[DisplayName("职位")]
public string? Position { get; set; }
/// <summary>
/// 职级
/// </summary>
[DisplayName("职级")]
public string? JobLevel { get; set; }
/// <summary>
/// 入职日期
/// </summary>
[DisplayName("入职日期")]
[DateColumn(Format = "YYYY-MM-DD")]
public DateTime? HireDate { get; set; }
/// <summary>
/// 在职状态
/// </summary>
[DisplayName("在职状态")]
public EmploymentStatus EmploymentStatus { get; set; }
/// <summary>
/// 是否激活
/// </summary>
[DisplayName("是否激活")]
public bool IsActive { get; set; }
/// <summary>
/// 创建时间
/// </summary>
[DisplayName("创建时间")]
[DateColumn(FromNow = true)]
public DateTime CreatedAt { get; set; }
/// <summary>
/// 更新时间
/// </summary>
[DisplayName("更新时间")]
[DateColumn(FromNow = true)]
public DateTime? UpdatedAt { get; set; }
}
说明:
列特性(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:数据源URLValueField:值字段名LabelField:标签字段名Multiple:是否多选Searchable:是否可搜索ShowOutline:是否显示轮廓SubmitOnChange:选择后是否自动提交
-
AmisSelectField:下拉选择组件Source:数据源URLValueField:值字段名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:数据源URLSubmitOnChange:选择后自动提交查询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<ApplicationUser> _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<ApplicationUser> _userManager;
/// <summary>
/// 构造函数
/// </summary>
public EmployeeService(
IRepository<Employee> employeeRepository,
IRepository<Department> departmentRepository,
IRepository<ApplicationUser> userRepository,
IMapper mapper,
ILogger<EmployeeService> logger,
IIdGenerator idGenerator,
ICurrentUser currentUser,
ApplicationDbContext dbContext,
IDepartmentService departmentService,
UserManager<ApplicationUser> 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<ActionResult<ApiResponse<PageList<EmployeeDto>>>> 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<ActionResult<ApiResponse<List<EmployeeDto>>>> 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<ActionResult<ApiResponse<EmployeeDto>>> 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<ActionResult<ApiResponse<EmployeeDto>>> 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<ActionResult<ApiResponse>> 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<ActionResult<ApiResponse>> 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<ActionResult<ApiResponse>> 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<ActionResult<ApiResponse>> 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<ActionResult<ApiResponse>> 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<ActionResult<ApiResponse>> 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<T>统一响应 - ✅ 分页查询:支持分页、排序、多条件筛选
- ✅ 批量操作:支持批量删除、批量导入等操作
- ✅ 异常处理:统一的异常处理和错误响应
- ✅ 权限控制:支持基于特性的权限控制
- ✅ 审计日志:自动记录创建、更新、删除操作
- ✅ 多租户支持:自动进行数据隔离
- ✅ 软删除支持:删除操作使用软删除,数据可恢复
标准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<ActionResult<ApiResponse<EmployeeDto>>> 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:是否在初始化时提交,默认为falseWrapWithPanel:是否不使用面板包装,默认为falseAsideResizor:侧边栏宽度是否可调整,默认为trueAsideMinWidth:侧边栏最小宽度(像素),默认为0AsideMaxWidth:侧边栏最大宽度(像素),默认为0AsideSticky:侧边栏是否固定,默认为trueAsidePosition:侧边栏位置(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抛出业务异常 - 在数据库层面创建唯一索引确保数据完整性
-
数据库设计:
- 为常用查询字段创建索引提升性能
- 合理配置关联关系的级联删除策略
- 使用复合唯一索引确保租户内业务字段唯一性
-
文档注释:
- 为所有公共成员添加XML文档注释
- 使用
<summary>、<param>、<returns>标签
相关文档
总结
通过CodeSpirit框架的BaseCRUDIService和标准开发模式,您可以快速开发出功能完整的CRUD接口。职工管理模块展示了:
- ✅ 标准CRUD操作的实现
- ✅ 关联关系管理(部门、用户账号)
- ✅ 业务验证逻辑的编写(工号唯一性、部门存在性等)
- ✅ 多条件查询的实现(关键字、部门、状态、日期范围等)
- ✅ 表单分组展示的使用
- ✅ 额外业务操作的实现(设置激活状态、转移部门、办理离职等)
- ✅ AMIS特性的使用(表格列、表单字段、图片上传等)
框架会自动处理大部分样板代码,让您专注于业务逻辑的实现。
更多交流请关注“CodeSpirit-码灵”公众号进群!!!
祝您开发愉快!🚀
出处:http://www.cnblogs.com/codelove/
如果喜欢作者的文章,请关注【CodeSpirit-码灵】公众号以便第一时间获得最新内容。本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
静听鸟语花香,漫赏云卷云舒。

浙公网安备 33010602011771号