CodeSpirit CRUD开发完整指南包含哪些内容?

摘要:概述 本文档通过职工管理(Employee)的实际代码示例,展示如何使用CodeSpirit框架快速开发CRUD功能。该示例来自身份认证系统(IdentityApi),是一个标准的关联型CRUD模块,包含完整的验证逻辑、业务处理和关联关系管
概述 本文档通过职工管理(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目录下创建实体类: // 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:数据源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<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:是否在初始化时提交,默认为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抛出业务异常 在数据库层面创建唯一索引确保数据完整性 数据库设计: 为常用查询字段创建索引提升性能 合理配置关联关系的级联删除策略 使用复合唯一索引确保租户内业务字段唯一性 文档注释: 为所有公共成员添加XML文档注释 使用<summary>、<param>、<returns>标签 相关文档 CodeSpirit.Core核心框架 开发环境搭建指南 项目整体架构设计 统一异常处理指南 总结 通过CodeSpirit框架的BaseCRUDIService和标准开发模式,您可以快速开发出功能完整的CRUD接口。职工管理模块展示了: ✅ 标准CRUD操作的实现 ✅ 关联关系管理(部门、用户账号) ✅ 业务验证逻辑的编写(工号唯一性、部门存在性等) ✅ 多条件查询的实现(关键字、部门、状态、日期范围等) ✅ 表单分组展示的使用 ✅ 额外业务操作的实现(设置激活状态、转移部门、办理离职等) ✅ AMIS特性的使用(表格列、表单字段、图片上传等) 框架会自动处理大部分样板代码,让您专注于业务逻辑的实现。 更多交流请关注“CodeSpirit-码灵”公众号进群!!! 祝您开发愉快!🚀