找回密码
 立即注册
首页 业界区 业界 CodeSpirit CRUD开发完整指南

CodeSpirit CRUD开发完整指南

柏球侠 3 天前
概述

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

开发流程概览

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目录下创建实体类:
  1. // Data/Models/Employee.cs
  2. using CodeSpirit.Shared.Entities.Interfaces;
  3. using CodeSpirit.MultiTenant.Abstractions;
  4. using System.ComponentModel.DataAnnotations;
  5. namespace CodeSpirit.IdentityApi.Data.Models;
  6. /// <summary>
  7. /// 职工信息
  8. /// </summary>
  9. public class Employee : IFullAuditable, IMultiTenant, IIsActive
  10. {
  11.     /// <summary>
  12.     /// 职工ID
  13.     /// </summary>
  14.     public long Id { get; set; }
  15.     /// <summary>
  16.     /// 租户ID(多租户支持)
  17.     /// </summary>
  18.     [Required]
  19.     [MaxLength(50)]
  20.     public string TenantId { get; set; } = string.Empty;
  21.     /// <summary>
  22.     /// 工号(租户内唯一)
  23.     /// </summary>
  24.     [Required]
  25.     [MaxLength(50)]
  26.     public string EmployeeNo { get; set; } = string.Empty;
  27.     /// <summary>
  28.     /// 姓名
  29.     /// </summary>
  30.     [Required]
  31.     [MaxLength(100)]
  32.     public string Name { get; set; } = string.Empty;
  33.     /// <summary>
  34.     /// 性别
  35.     /// </summary>
  36.     public Gender Gender { get; set; }
  37.     /// <summary>
  38.     /// 身份证号码
  39.     /// </summary>
  40.     [MaxLength(18)]
  41.     public string? IdNo { get; set; }
  42.     /// <summary>
  43.     /// 出生日期
  44.     /// </summary>
  45.     public DateTime? BirthDate { get; set; }
  46.     /// <summary>
  47.     /// 手机号码
  48.     /// </summary>
  49.     [MaxLength(15)]
  50.     public string? PhoneNumber { get; set; }
  51.     /// <summary>
  52.     /// 电子邮箱
  53.     /// </summary>
  54.     [MaxLength(100)]
  55.     [EmailAddress]
  56.     public string? Email { get; set; }
  57.     /// <summary>
  58.     /// 部门ID
  59.     /// </summary>
  60.     public long? DepartmentId { get; set; }
  61.     /// <summary>
  62.     /// 所属部门(导航属性)
  63.     /// </summary>
  64.     public Department? Department { get; set; }
  65.     /// <summary>
  66.     /// 职位
  67.     /// </summary>
  68.     [MaxLength(100)]
  69.     public string? Position { get; set; }
  70.     /// <summary>
  71.     /// 职级
  72.     /// </summary>
  73.     [MaxLength(50)]
  74.     public string? JobLevel { get; set; }
  75.     /// <summary>
  76.     /// 入职日期
  77.     /// </summary>
  78.     public DateTime? HireDate { get; set; }
  79.     /// <summary>
  80.     /// 离职日期
  81.     /// </summary>
  82.     public DateTime? TerminationDate { get; set; }
  83.     /// <summary>
  84.     /// 在职状态
  85.     /// </summary>
  86.     public EmploymentStatus EmploymentStatus { get; set; }
  87.     /// <summary>
  88.     /// 关联的用户ID
  89.     /// </summary>
  90.     public long? UserId { get; set; }
  91.     /// <summary>
  92.     /// 关联的用户账号(导航属性)
  93.     /// </summary>
  94.     public ApplicationUser? User { get; set; }
  95.     /// <summary>
  96.     /// 紧急联系人
  97.     /// </summary>
  98.     [MaxLength(100)]
  99.     public string? EmergencyContact { get; set; }
  100.     /// <summary>
  101.     /// 紧急联系电话
  102.     /// </summary>
  103.     [MaxLength(15)]
  104.     public string? EmergencyPhone { get; set; }
  105.     /// <summary>
  106.     /// 地址
  107.     /// </summary>
  108.     [MaxLength(500)]
  109.     public string? Address { get; set; }
  110.     /// <summary>
  111.     /// 备注
  112.     /// </summary>
  113.     [MaxLength(1000)]
  114.     public string? Remarks { get; set; }
  115.     /// <summary>
  116.     /// 是否激活
  117.     /// </summary>
  118.     public bool IsActive { get; set; } = true;
  119.     /// <summary>
  120.     /// 头像地址
  121.     /// </summary>
  122.     [MaxLength(255)]
  123.     [DataType(DataType.ImageUrl)]
  124.     public string? AvatarUrl { get; set; }
  125.     // 审计字段(实现IFullAuditable接口)
  126.     public long CreatedBy { get; set; }
  127.     public DateTime CreatedAt { get; set; }
  128.     public long? UpdatedBy { get; set; }
  129.     public DateTime? UpdatedAt { get; set; }
  130.     public long? DeletedBy { get; set; }
  131.     public DateTime? DeletedAt { get; set; }
  132.     public bool IsDeleted { get; set; }
  133. }
复制代码
说明

  • 实现IFullAuditable接口,自动包含完整的审计字段(创建、更新、删除)
  • 实现IMultiTenant接口,支持多租户数据隔离
  • 实现IIsActive接口,支持激活状态管理
  • 使用long作为主键类型
  • 包含关联关系的导航属性(部门、用户账号)
  • 支持软删除(IsDeleted字段)
2. 创建DTO类

在Dtos/Employee目录下创建DTO类:
2.1 EmployeeDto(展示DTO)
  1. // Dtos/Employee/EmployeeDto.cs
  2. using CodeSpirit.Amis.Attributes.Columns;
  3. using CodeSpirit.Core.Attributes;
  4. using CodeSpirit.IdentityApi.Data.Models;
  5. using System.ComponentModel;
  6. namespace CodeSpirit.IdentityApi.Dtos.Employee;
  7. /// <summary>
  8. /// 职工数据传输对象
  9. /// </summary>
  10. public class EmployeeDto
  11. {
  12.     /// <summary>
  13.     /// 职工ID
  14.     /// </summary>
  15.     public long Id { get; set; }
  16.     /// <summary>
  17.     /// 工号
  18.     /// </summary>
  19.     [DisplayName("工号")]
  20.     public string EmployeeNo { get; set; } = string.Empty;
  21.     /// <summary>
  22.     /// 姓名
  23.     /// </summary>
  24.     [DisplayName("姓名")]
  25.     [TplColumn(template: "${name}")]
  26.     public string Name { get; set; } = string.Empty;
  27.     /// <summary>
  28.     /// 头像地址
  29.     /// </summary>
  30.     [DisplayName("头像")]
  31.     [AvatarColumn(Text = "${name}", Src = "${avatarUrl}")]
  32.     [Badge(Animation = true, VisibleOn = "isActive", Level = "info")]
  33.     public string? AvatarUrl { get; set; }
  34.     /// <summary>
  35.     /// 性别
  36.     /// </summary>
  37.     [DisplayName("性别")]
  38.     public Gender Gender { get; set; }
  39.     /// <summary>
  40.     /// 手机号码
  41.     /// </summary>
  42.     [DisplayName("手机号码")]
  43.     public string? PhoneNumber { get; set; }
  44.     /// <summary>
  45.     /// 电子邮箱
  46.     /// </summary>
  47.     [DisplayName("电子邮箱")]
  48.     public string? Email { get; set; }
  49.     /// <summary>
  50.     /// 部门ID
  51.     /// </summary>
  52.     [AmisColumn(Hidden = true)]
  53.     public long? DepartmentId { get; set; }
  54.     /// <summary>
  55.     /// 部门名称
  56.     /// </summary>
  57.     [DisplayName("部门")]
  58.     public string? DepartmentName { get; set; }
  59.     /// <summary>
  60.     /// 职位
  61.     /// </summary>
  62.     [DisplayName("职位")]
  63.     public string? Position { get; set; }
  64.     /// <summary>
  65.     /// 职级
  66.     /// </summary>
  67.     [DisplayName("职级")]
  68.     public string? JobLevel { get; set; }
  69.     /// <summary>
  70.     /// 入职日期
  71.     /// </summary>
  72.     [DisplayName("入职日期")]
  73.     [DateColumn(Format = "YYYY-MM-DD")]
  74.     public DateTime? HireDate { get; set; }
  75.     /// <summary>
  76.     /// 在职状态
  77.     /// </summary>
  78.     [DisplayName("在职状态")]
  79.     public EmploymentStatus EmploymentStatus { get; set; }
  80.     /// <summary>
  81.     /// 是否激活
  82.     /// </summary>
  83.     [DisplayName("是否激活")]
  84.     public bool IsActive { get; set; }
  85.     /// <summary>
  86.     /// 创建时间
  87.     /// </summary>
  88.     [DisplayName("创建时间")]
  89.     [DateColumn(FromNow = true)]
  90.     public DateTime CreatedAt { get; set; }
  91.     /// <summary>
  92.     /// 更新时间
  93.     /// </summary>
  94.     [DisplayName("更新时间")]
  95.     [DateColumn(FromNow = true)]
  96.     public DateTime? UpdatedAt { get; set; }
  97. }
复制代码
说明
列特性(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)

2.png
  1. // Dtos/Employee/CreateEmployeeDto.cs
  2. using CodeSpirit.Amis.Attributes.FormFields;
  3. using CodeSpirit.IdentityApi.Data.Models;
  4. using System.ComponentModel;
  5. using System.ComponentModel.DataAnnotations;
  6. namespace CodeSpirit.IdentityApi.Dtos.Employee;
  7. /// <summary>
  8. /// 创建职工数据传输对象
  9. /// </summary>
  10. [FormGroup("basic", "基本信息", "EmployeeNo,Name,Gender,IdNo,BirthDate", Order = 1)]
  11. [FormGroup("contact", "联系方式", "PhoneNumber,Email,Address", Order = 2)]
  12. [FormGroup("work", "工作信息", "DepartmentId,Position,JobLevel,HireDate,EmploymentStatus", Order = 3)]
  13. [FormGroup("relation", "关联信息", "UserId", Order = 4)]
  14. [FormGroup("emergency", "紧急联系人", "EmergencyContact,EmergencyPhone", Order = 5)]
  15. [FormGroup("other", "其他信息", "AvatarUrl,Remarks,IsActive", Order = 6)]
  16. public class CreateEmployeeDto
  17. {
  18.     /// <summary>
  19.     /// 工号
  20.     /// </summary>
  21.     [Required(ErrorMessage = "工号不能为空")]
  22.     [MaxLength(50, ErrorMessage = "工号长度不能超过50个字符")]
  23.     [DisplayName("工号")]
  24.     [AmisInputTextField(ColumnRatio = 6)]
  25.     public string EmployeeNo { get; set; } = string.Empty;
  26.     /// <summary>
  27.     /// 姓名
  28.     /// </summary>
  29.     [Required(ErrorMessage = "姓名不能为空")]
  30.     [MaxLength(100, ErrorMessage = "姓名长度不能超过100个字符")]
  31.     [DisplayName("姓名")]
  32.     [AmisInputTextField(ColumnRatio = 6)]
  33.     public string Name { get; set; } = string.Empty;
  34.     /// <summary>
  35.     /// 性别
  36.     /// </summary>
  37.     [DisplayName("性别")]
  38.     [AmisFormField(ColumnRatio = 6)]
  39.     public Gender Gender { get; set; }
  40.     /// <summary>
  41.     /// 身份证号码
  42.     /// </summary>
  43.     [MaxLength(18, ErrorMessage = "身份证号码长度不能超过18个字符")]
  44.     [DisplayName("身份证号")]
  45.     [AmisInputTextField(ColumnRatio = 6)]
  46.     public string? IdNo { get; set; }
  47.     /// <summary>
  48.     /// 出生日期
  49.     /// </summary>
  50.     [DisplayName("出生日期")]
  51.     [AmisDateFieldAttribute(ColumnRatio = 6)]
  52.     public DateTime? BirthDate { get; set; }
  53.     /// <summary>
  54.     /// 手机号码
  55.     /// </summary>
  56.     [MaxLength(15, ErrorMessage = "手机号码长度不能超过15个字符")]
  57.     [Phone(ErrorMessage = "手机号码格式不正确")]
  58.     [DisplayName("手机号码")]
  59.     [AmisInputTextField(ColumnRatio = 6)]
  60.     public string? PhoneNumber { get; set; }
  61.     /// <summary>
  62.     /// 电子邮箱
  63.     /// </summary>
  64.     [MaxLength(100, ErrorMessage = "电子邮箱长度不能超过100个字符")]
  65.     [EmailAddress(ErrorMessage = "电子邮箱格式不正确")]
  66.     [DisplayName("电子邮箱")]
  67.     [AmisInputTextField(ColumnRatio = 6)]
  68.     public string? Email { get; set; }
  69.     /// <summary>
  70.     /// 部门ID
  71.     /// </summary>
  72.     [DisplayName("部门")]
  73.     [AmisInputTreeField(
  74.         DataSource = "${ROOT_API}/api/identity/Departments/tree",
  75.         LabelField = "name",
  76.         ValueField = "id",
  77.         Multiple = false,
  78.         Searchable = true,
  79.         ColumnRatio = 12
  80.     )]
  81.     public long? DepartmentId { get; set; }
  82.     /// <summary>
  83.     /// 职位
  84.     /// </summary>
  85.     [MaxLength(100, ErrorMessage = "职位长度不能超过100个字符")]
  86.     [DisplayName("职位")]
  87.     [AmisInputTextField(ColumnRatio = 6)]
  88.     public string? Position { get; set; }
  89.     /// <summary>
  90.     /// 职级
  91.     /// </summary>
  92.     [MaxLength(50, ErrorMessage = "职级长度不能超过50个字符")]
  93.     [DisplayName("职级")]
  94.     [AmisInputTextField(ColumnRatio = 6)]
  95.     public string? JobLevel { get; set; }
  96.     /// <summary>
  97.     /// 入职日期
  98.     /// </summary>
  99.     [DisplayName("入职日期")]
  100.     [AmisDateFieldAttribute(ColumnRatio = 6)]
  101.     public DateTime? HireDate { get; set; }
  102.     /// <summary>
  103.     /// 在职状态
  104.     /// </summary>
  105.     [DisplayName("在职状态")]
  106.     [AmisFormField(ColumnRatio = 6)]
  107.     public EmploymentStatus EmploymentStatus { get; set; } = EmploymentStatus.Active;
  108.     /// <summary>
  109.     /// 关联的用户ID
  110.     /// </summary>
  111.     [DisplayName("关联用户")]
  112.     [AmisSelectField(
  113.         Source = "${ROOT_API}/api/identity/Users",
  114.         ValueField = "id",
  115.         LabelField = "name",
  116.         Multiple = false,
  117.         Searchable = true,
  118.         ColumnRatio = 12
  119.     )]
  120.     public long? UserId { get; set; }
  121.     /// <summary>
  122.     /// 紧急联系人
  123.     /// </summary>
  124.     [MaxLength(100, ErrorMessage = "紧急联系人长度不能超过100个字符")]
  125.     [DisplayName("紧急联系人")]
  126.     [AmisInputTextField(ColumnRatio = 6)]
  127.     public string? EmergencyContact { get; set; }
  128.     /// <summary>
  129.     /// 紧急联系电话
  130.     /// </summary>
  131.     [MaxLength(15, ErrorMessage = "紧急联系电话长度不能超过15个字符")]
  132.     [Phone(ErrorMessage = "紧急联系电话格式不正确")]
  133.     [DisplayName("紧急联系电话")]
  134.     [AmisInputTextField(ColumnRatio = 6)]
  135.     public string? EmergencyPhone { get; set; }
  136.     /// <summary>
  137.     /// 地址
  138.     /// </summary>
  139.     [MaxLength(500, ErrorMessage = "地址长度不能超过500个字符")]
  140.     [DisplayName("地址")]
  141.     [AmisTextareaField(ColumnRatio = 12)]
  142.     public string? Address { get; set; }
  143.     /// <summary>
  144.     /// 头像地址
  145.     /// </summary>
  146.     [MaxLength(255, ErrorMessage = "头像地址长度不能超过255个字符")]
  147.     [DisplayName("头像")]
  148.     [AmisInputImageField(
  149.         Receiver = "/file/api/file/images/upload?BucketName=avatar",
  150.         Accept = "image/png,image/jpeg,image/jpg",
  151.         MaxSize = 2097152,
  152.         Multiple = false,
  153.         ColumnRatio = 12
  154.     )]
  155.     public string? AvatarUrl { get; set; }
  156.     /// <summary>
  157.     /// 备注
  158.     /// </summary>
  159.     [MaxLength(1000, ErrorMessage = "备注长度不能超过1000个字符")]
  160.     [DisplayName("备注")]
  161.     [AmisTextareaField(ColumnRatio = 12)]
  162.     public string? Remarks { get; set; }
  163.     /// <summary>
  164.     /// 是否激活
  165.     /// </summary>
  166.     [DisplayName("是否激活")]
  167.     [AmisFormField(ColumnRatio = 6)]
  168.     public bool IsActive { get; set; } = true;
  169. }
复制代码
说明
表单特性(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)
  1. // Dtos/Employee/UpdateEmployeeDto.cs
  2. using CodeSpirit.Amis.Attributes.FormFields;
  3. using CodeSpirit.IdentityApi.Data.Models;
  4. using System.ComponentModel;
  5. using System.ComponentModel.DataAnnotations;
  6. namespace CodeSpirit.IdentityApi.Dtos.Employee;
  7. /// <summary>
  8. /// 更新职工数据传输对象
  9. /// </summary>
  10. [FormGroup("basic", "基本信息", "EmployeeNo,Name,Gender,IdNo,BirthDate", Order = 1)]
  11. [FormGroup("contact", "联系方式", "PhoneNumber,Email,Address", Order = 2)]
  12. [FormGroup("work", "工作信息", "DepartmentId,Position,JobLevel,HireDate,TerminationDate,EmploymentStatus", Order = 3)]
  13. [FormGroup("relation", "关联信息", "UserId", Order = 4)]
  14. [FormGroup("emergency", "紧急联系人", "EmergencyContact,EmergencyPhone", Order = 5)]
  15. [FormGroup("other", "其他信息", "AvatarUrl,Remarks,IsActive", Order = 6)]
  16. public class UpdateEmployeeDto
  17. {
  18.     /// <summary>
  19.     /// 工号
  20.     /// </summary>
  21.     [Required(ErrorMessage = "工号不能为空")]
  22.     [MaxLength(50, ErrorMessage = "工号长度不能超过50个字符")]
  23.     [DisplayName("工号")]
  24.     [AmisInputTextField(ColumnRatio = 6)]
  25.     public string EmployeeNo { get; set; } = string.Empty;
  26.     /// <summary>
  27.     /// 姓名
  28.     /// </summary>
  29.     [Required(ErrorMessage = "姓名不能为空")]
  30.     [MaxLength(100, ErrorMessage = "姓名长度不能超过100个字符")]
  31.     [DisplayName("姓名")]
  32.     [AmisInputTextField(ColumnRatio = 6)]
  33.     public string Name { get; set; } = string.Empty;
  34.     /// <summary>
  35.     /// 性别
  36.     /// </summary>
  37.     [DisplayName("性别")]
  38.     [AmisFormField(ColumnRatio = 6)]
  39.     public Gender Gender { get; set; }
  40.     /// <summary>
  41.     /// 身份证号码
  42.     /// </summary>
  43.     [MaxLength(18, ErrorMessage = "身份证号码长度不能超过18个字符")]
  44.     [DisplayName("身份证号")]
  45.     [AmisInputTextField(ColumnRatio = 6)]
  46.     public string? IdNo { get; set; }
  47.     /// <summary>
  48.     /// 出生日期
  49.     /// </summary>
  50.     [DisplayName("出生日期")]
  51.     [AmisDateFieldAttribute(ColumnRatio = 6)]
  52.     public DateTime? BirthDate { get; set; }
  53.     /// <summary>
  54.     /// 手机号码
  55.     /// </summary>
  56.     [MaxLength(15, ErrorMessage = "手机号码长度不能超过15个字符")]
  57.     [Phone(ErrorMessage = "手机号码格式不正确")]
  58.     [DisplayName("手机号码")]
  59.     [AmisInputTextField(ColumnRatio = 6)]
  60.     public string? PhoneNumber { get; set; }
  61.     /// <summary>
  62.     /// 电子邮箱
  63.     /// </summary>
  64.     [MaxLength(100, ErrorMessage = "电子邮箱长度不能超过100个字符")]
  65.     [EmailAddress(ErrorMessage = "电子邮箱格式不正确")]
  66.     [DisplayName("电子邮箱")]
  67.     [AmisInputTextField(ColumnRatio = 6)]
  68.     public string? Email { get; set; }
  69.     /// <summary>
  70.     /// 部门ID
  71.     /// </summary>
  72.     [DisplayName("部门")]
  73.     [AmisInputTreeField(
  74.         DataSource = "${ROOT_API}/api/identity/Departments/tree",
  75.         LabelField = "name",
  76.         ValueField = "id",
  77.         Multiple = false,
  78.         Searchable = true,
  79.         ColumnRatio = 12
  80.     )]
  81.     public long? DepartmentId { get; set; }
  82.     /// <summary>
  83.     /// 职位
  84.     /// </summary>
  85.     [MaxLength(100, ErrorMessage = "职位长度不能超过100个字符")]
  86.     [DisplayName("职位")]
  87.     [AmisInputTextField(ColumnRatio = 6)]
  88.     public string? Position { get; set; }
  89.     /// <summary>
  90.     /// 职级
  91.     /// </summary>
  92.     [MaxLength(50, ErrorMessage = "职级长度不能超过50个字符")]
  93.     [DisplayName("职级")]
  94.     [AmisInputTextField(ColumnRatio = 6)]
  95.     public string? JobLevel { get; set; }
  96.     /// <summary>
  97.     /// 入职日期
  98.     /// </summary>
  99.     [DisplayName("入职日期")]
  100.     [AmisDateFieldAttribute(ColumnRatio = 6)]
  101.     public DateTime? HireDate { get; set; }
  102.     /// <summary>
  103.     /// 离职日期
  104.     /// </summary>
  105.     [DisplayName("离职日期")]
  106.     [AmisDateFieldAttribute(ColumnRatio = 6)]
  107.     public DateTime? TerminationDate { get; set; }
  108.     /// <summary>
  109.     /// 在职状态
  110.     /// </summary>
  111.     [DisplayName("在职状态")]
  112.     [AmisFormField(ColumnRatio = 12)]
  113.     public EmploymentStatus EmploymentStatus { get; set; }
  114.     /// <summary>
  115.     /// 关联的用户ID
  116.     /// </summary>
  117.     [DisplayName("关联用户")]
  118.     [AmisSelectField(
  119.         Source = "${ROOT_API}/api/identity/Users",
  120.         ValueField = "id",
  121.         LabelField = "name",
  122.         Multiple = false,
  123.         Searchable = true,
  124.         ColumnRatio = 12
  125.     )]
  126.     public long? UserId { get; set; }
  127.     /// <summary>
  128.     /// 紧急联系人
  129.     /// </summary>
  130.     [MaxLength(100, ErrorMessage = "紧急联系人长度不能超过100个字符")]
  131.     [DisplayName("紧急联系人")]
  132.     [AmisInputTextField(ColumnRatio = 6)]
  133.     public string? EmergencyContact { get; set; }
  134.     /// <summary>
  135.     /// 紧急联系电话
  136.     /// </summary>
  137.     [MaxLength(15, ErrorMessage = "紧急联系电话长度不能超过15个字符")]
  138.     [Phone(ErrorMessage = "紧急联系电话格式不正确")]
  139.     [DisplayName("紧急联系电话")]
  140.     [AmisInputTextField(ColumnRatio = 6)]
  141.     public string? EmergencyPhone { get; set; }
  142.     /// <summary>
  143.     /// 地址
  144.     /// </summary>
  145.     [MaxLength(500, ErrorMessage = "地址长度不能超过500个字符")]
  146.     [DisplayName("地址")]
  147.     [AmisTextareaField(ColumnRatio = 12)]
  148.     public string? Address { get; set; }
  149.     /// <summary>
  150.     /// 头像地址
  151.     /// </summary>
  152.     [MaxLength(255, ErrorMessage = "头像地址长度不能超过255个字符")]
  153.     [DisplayName("头像")]
  154.     [AmisInputImageField(
  155.         Receiver = "/file/api/file/images/upload?BucketName=avatar",
  156.         Accept = "image/png,image/jpeg,image/jpg",
  157.         MaxSize = 2097152,
  158.         Multiple = false,
  159.         ColumnRatio = 12
  160.     )]
  161.     public string? AvatarUrl { get; set; }
  162.     /// <summary>
  163.     /// 备注
  164.     /// </summary>
  165.     [MaxLength(1000, ErrorMessage = "备注长度不能超过1000个字符")]
  166.     [DisplayName("备注")]
  167.     [AmisTextareaField(ColumnRatio = 12)]
  168.     public string? Remarks { get; set; }
  169.     /// <summary>
  170.     /// 是否激活
  171.     /// </summary>
  172.     [DisplayName("是否激活")]
  173.     [AmisFormField(ColumnRatio = 6)]
  174.     public bool IsActive { get; set; }
  175. }
复制代码
2.4 EmployeeQueryDto(查询DTO)
  1. // Dtos/Employee/EmployeeQueryDto.cs
  2. using CodeSpirit.Amis.Attributes.FormFields;
  3. using CodeSpirit.Core.Dtos;
  4. using CodeSpirit.IdentityApi.Data.Models;
  5. using System.ComponentModel;
  6. namespace CodeSpirit.IdentityApi.Dtos.Employee;
  7. /// <summary>
  8. /// 职工查询数据传输对象
  9. /// </summary>
  10. public class EmployeeQueryDto : QueryDtoBase
  11. {
  12.     /// <summary>
  13.     /// 关键字搜索(姓名、工号、身份证、手机、邮箱)
  14.     /// </summary>
  15.     [DisplayName("关键字")]
  16.     public string? Keywords { get; set; }
  17.     /// <summary>
  18.     /// 是否激活
  19.     /// </summary>
  20.     [DisplayName("是否激活")]
  21.     public bool? IsActive { get; set; }
  22.     /// <summary>
  23.     /// 性别筛选
  24.     /// </summary>
  25.     [DisplayName("性别")]
  26.     public Gender? Gender { get; set; }
  27.     /// <summary>
  28.     /// 部门ID筛选
  29.     /// </summary>
  30.     [DisplayName("部门")]
  31.     [AmisInputTreeField(
  32.         DataSource = "${ROOT_API}/api/identity/Departments/tree",
  33.         Multiple = false,
  34.         JoinValues = true,
  35.         ExtractValue = false,
  36.         ShowOutline = true,
  37.         LabelField = "name",
  38.         ValueField = "id",
  39.         Required = false,
  40.         Clearable = true,
  41.         SubmitOnChange = true,
  42.         HeightAuto = true,
  43.         SelectFirst = false,
  44.         InputOnly = true,
  45.         ShowIcon = true
  46.     )]
  47.     [PageAside()]
  48.     public long? DepartmentId { get; set; }
  49.     /// <summary>
  50.     /// 在职状态筛选
  51.     /// </summary>
  52.     [DisplayName("在职状态")]
  53.     public EmploymentStatus? EmploymentStatus { get; set; }
  54.     /// <summary>
  55.     /// 入职日期范围
  56.     /// </summary>
  57.     [DisplayName("入职日期")]
  58.     public DateTime[]? HireDate { get; set; }
  59.     /// <summary>
  60.     /// 职位
  61.     /// </summary>
  62.     [DisplayName("职位")]
  63.     public string? Position { get; set; }
  64.     /// <summary>
  65.     /// 职级
  66.     /// </summary>
  67.     [DisplayName("职级")]
  68.     public string? JobLevel { get; set; }
  69. }
复制代码
说明
查询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目录下创建映射配置:
  1. // MappingProfiles/EmployeeProfile.cs
  2. using AutoMapper;
  3. using CodeSpirit.IdentityApi.Data.Models;
  4. using CodeSpirit.IdentityApi.Dtos.Employee;
  5. using CodeSpirit.Shared.Extensions;
  6. namespace CodeSpirit.IdentityApi.MappingProfiles;
  7. /// <summary>
  8. /// 职工映射配置
  9. /// </summary>
  10. public class EmployeeProfile : Profile
  11. {
  12.     /// <summary>
  13.     /// 构造函数
  14.     /// </summary>
  15.     public EmployeeProfile()
  16.     {
  17.         // 使用扩展方法配置基本CRUD映射(自动处理Include导航属性)
  18.         this.ConfigureBaseCRUDIMappings<
  19.             Employee,
  20.             EmployeeDto,
  21.             long,
  22.             CreateEmployeeDto,
  23.             UpdateEmployeeDto,
  24.             CreateEmployeeDto>();
  25.             
  26.         // 自定义映射:映射部门名称和用户名
  27.         CreateMap<Employee, EmployeeDto>()
  28.             .ForMember(dest => dest.DepartmentName, opt => opt.MapFrom(src => src.Department != null ? src.Department.Name : null))
  29.             .ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.User != null ? src.User.UserName : null));
  30.     }
  31. }
复制代码
说明

  • ConfigureBaseCRUDIMappings扩展方法自动配置基本的CRUD映射
  • 使用ForMember自定义字段映射逻辑,将导航属性映射到DTO
  • 支持多个DTO类型的映射配置
4. 创建服务接口和实现

4.1 服务接口
  1. // Services/IEmployeeService.cs
  2. using CodeSpirit.Core;
  3. using CodeSpirit.IdentityApi.Data.Models;
  4. using CodeSpirit.IdentityApi.Dtos.Employee;
  5. using CodeSpirit.Shared.Services;
  6. namespace CodeSpirit.IdentityApi.Services;
  7. /// <summary>
  8. /// 职工服务接口
  9. /// </summary>
  10. public interface IEmployeeService : IBaseCRUDIService<Employee, EmployeeDto, long, CreateEmployeeDto, UpdateEmployeeDto, EmployeeBatchImportItemDto>, IScopedDependency
  11. {
  12.     /// <summary>
  13.     /// 获取职工列表(分页)
  14.     /// </summary>
  15.     /// <param name="queryDto">查询条件</param>
  16.     /// <returns>职工分页列表</returns>
  17.     Task<PageList<EmployeeDto>> GetEmployeesAsync(EmployeeQueryDto queryDto);
  18.     /// <summary>
  19.     /// 根据部门获取职工列表
  20.     /// </summary>
  21.     /// <param name="departmentId">部门ID</param>
  22.     /// <param name="includeSubDepartments">是否包含子部门</param>
  23.     /// <returns>职工列表</returns>
  24.     Task<List<EmployeeDto>> GetEmployeesByDepartmentAsync(long departmentId, bool includeSubDepartments = false);
  25.     /// <summary>
  26.     /// 设置职工激活状态
  27.     /// </summary>
  28.     /// <param name="id">职工ID</param>
  29.     /// <param name="isActive">是否激活</param>
  30.     Task SetActiveStatusAsync(long id, bool isActive);
  31.     /// <summary>
  32.     /// 转移职工到新部门
  33.     /// </summary>
  34.     /// <param name="employeeId">职工ID</param>
  35.     /// <param name="newDepartmentId">新部门ID</param>
  36.     Task TransferEmployeeAsync(long employeeId, long? newDepartmentId);
  37.     /// <summary>
  38.     /// 办理职工离职
  39.     /// </summary>
  40.     /// <param name="employeeId">职工ID</param>
  41.     /// <param name="terminationDate">离职日期</param>
  42.     Task TerminateEmployeeAsync(long employeeId, DateTime terminationDate);
  43.     /// <summary>
  44.     /// 验证工号是否唯一
  45.     /// </summary>
  46.     /// <param name="employeeNo">工号</param>
  47.     /// <param name="excludeId">排除的职工ID(用于更新时验证)</param>
  48.     /// <returns>是否唯一</returns>
  49.     Task<bool> IsEmployeeNoUniqueAsync(string employeeNo, long? excludeId = null);
  50. }
复制代码
4.2 服务实现
  1. // Services/EmployeeService.cs
  2. using AutoMapper;
  3. using CodeSpirit.Core;
  4. using CodeSpirit.Core.IdGenerator;
  5. using CodeSpirit.IdentityApi.Data;
  6. using CodeSpirit.IdentityApi.Data.Models;
  7. using CodeSpirit.IdentityApi.Dtos.Employee;
  8. using CodeSpirit.IdentityApi.Utilities;
  9. using CodeSpirit.Shared.Repositories;
  10. using CodeSpirit.Shared.Services;
  11. using CodeSpirit.Shared.Dtos.Common;
  12. using LinqKit;
  13. using Microsoft.AspNetCore.Identity;
  14. using Microsoft.EntityFrameworkCore;
  15. namespace CodeSpirit.IdentityApi.Services;
  16. /// <summary>
  17. /// 职工服务实现
  18. /// </summary>
  19. public class EmployeeService : BaseCRUDIService<Employee, EmployeeDto, long, CreateEmployeeDto, UpdateEmployeeDto, EmployeeBatchImportItemDto>, IEmployeeService
  20. {
  21.     private readonly IRepository<Employee> _employeeRepository;
  22.     private readonly IRepository<Department> _departmentRepository;
  23.     private readonly IRepository _userRepository;
  24.     private readonly ILogger<EmployeeService> _logger;
  25.     private readonly IIdGenerator _idGenerator;
  26.     private readonly ICurrentUser _currentUser;
  27.     private readonly ApplicationDbContext _dbContext;
  28.     private readonly IDepartmentService _departmentService;
  29.     private readonly UserManager _userManager;
  30.     /// <summary>
  31.     /// 构造函数
  32.     /// </summary>
  33.     public EmployeeService(
  34.         IRepository<Employee> employeeRepository,
  35.         IRepository<Department> departmentRepository,
  36.         IRepository userRepository,
  37.         IMapper mapper,
  38.         ILogger<EmployeeService> logger,
  39.         IIdGenerator idGenerator,
  40.         ICurrentUser currentUser,
  41.         ApplicationDbContext dbContext,
  42.         IDepartmentService departmentService,
  43.         UserManager userManager,
  44.         EnhancedBatchImportHelper<EmployeeBatchImportItemDto> importHelper)
  45.         : base(employeeRepository, mapper, importHelper)
  46.     {
  47.         _employeeRepository = employeeRepository;
  48.         _departmentRepository = departmentRepository;
  49.         _userRepository = userRepository;
  50.         _logger = logger;
  51.         _idGenerator = idGenerator;
  52.         _currentUser = currentUser;
  53.         _dbContext = dbContext;
  54.         _departmentService = departmentService;
  55.         _userManager = userManager;
  56.     }
  57.     /// <summary>
  58.     /// 获取职工列表(分页)
  59.     /// </summary>
  60.     public async Task<PageList<EmployeeDto>> GetEmployeesAsync(EmployeeQueryDto queryDto)
  61.     {
  62.         var predicate = PredicateBuilder.New<Employee>(true);
  63.         // 应用搜索关键词过滤
  64.         if (!string.IsNullOrWhiteSpace(queryDto.Keywords))
  65.         {
  66.             string searchLower = queryDto.Keywords.ToLower();
  67.             predicate = predicate.Or(e => e.Name.ToLower().Contains(searchLower));
  68.             predicate = predicate.Or(e => e.EmployeeNo.ToLower().Contains(searchLower));
  69.             predicate = predicate.Or(e => e.IdNo.Contains(queryDto.Keywords));
  70.             predicate = predicate.Or(e => e.PhoneNumber.Contains(queryDto.Keywords));
  71.             predicate = predicate.Or(e => e.Email.ToLower().Contains(searchLower));
  72.         }
  73.         // 应用其他过滤条件
  74.         if (queryDto.IsActive.HasValue)
  75.         {
  76.             predicate = predicate.And(e => e.IsActive == queryDto.IsActive.Value);
  77.         }
  78.         if (queryDto.Gender.HasValue)
  79.         {
  80.             predicate = predicate.And(e => e.Gender == queryDto.Gender.Value);
  81.         }
  82.         if (queryDto.DepartmentId.HasValue)
  83.         {
  84.             predicate = predicate.And(e => e.DepartmentId == queryDto.DepartmentId.Value);
  85.         }
  86.         if (queryDto.EmploymentStatus.HasValue)
  87.         {
  88.             predicate = predicate.And(e => e.EmploymentStatus == queryDto.EmploymentStatus.Value);
  89.         }
  90.         if (!string.IsNullOrWhiteSpace(queryDto.Position))
  91.         {
  92.             predicate = predicate.And(e => e.Position == queryDto.Position);
  93.         }
  94.         if (!string.IsNullOrWhiteSpace(queryDto.JobLevel))
  95.         {
  96.             predicate = predicate.And(e => e.JobLevel == queryDto.JobLevel);
  97.         }
  98.         if (queryDto.HireDate != null && queryDto.HireDate.Length == 2)
  99.         {
  100.             predicate = predicate.And(e => e.HireDate >= queryDto.HireDate[0]);
  101.             predicate = predicate.And(e => e.HireDate <= queryDto.HireDate[1]);
  102.         }
  103.         // 创建查询
  104.         var query = _employeeRepository.CreateQuery()
  105.             .Include(e => e.Department)
  106.             .Include(e => e.User)
  107.             .Where(predicate);
  108.         // 执行分页查询
  109.         var totalCount = await query.CountAsync();
  110.         var employees = await query
  111.             .OrderByDescending(e => e.CreatedAt)
  112.             .Skip((queryDto.Page - 1) * queryDto.PerPage)
  113.             .Take(queryDto.PerPage)
  114.             .ToListAsync();
  115.         // 映射到DTO
  116.         var employeeDtos = Mapper.Map<List<EmployeeDto>>(employees);
  117.         // 设置关联数据
  118.         foreach (var dto in employeeDtos)
  119.         {
  120.             var employee = employees.First(e => e.Id == dto.Id);
  121.             dto.DepartmentName = employee.Department?.Name;
  122.             dto.UserName = employee.User?.UserName;
  123.         }
  124.         return new PageList<EmployeeDto>(employeeDtos, totalCount);
  125.     }
  126.     /// <summary>
  127.     /// 根据部门获取职工列表
  128.     /// </summary>
  129.     public async Task<List<EmployeeDto>> GetEmployeesByDepartmentAsync(long departmentId, bool includeSubDepartments = false)
  130.     {
  131.         var departmentIds = new List<long> { departmentId };
  132.         
  133.         if (includeSubDepartments)
  134.         {
  135.             var subDepartments = await _departmentService.GetSubDepartmentsAsync(departmentId);
  136.             departmentIds.AddRange(subDepartments.Select(d => d.Id));
  137.         }
  138.         var employees = await _employeeRepository.CreateQuery()
  139.             .Include(e => e.Department)
  140.             .Include(e => e.User)
  141.             .Where(e => departmentIds.Contains(e.DepartmentId ?? 0))
  142.             .ToListAsync();
  143.         return Mapper.Map<List<EmployeeDto>>(employees);
  144.     }
  145.     /// <summary>
  146.     /// 设置职工激活状态
  147.     /// </summary>
  148.     public async Task SetActiveStatusAsync(long id, bool isActive)
  149.     {
  150.         var employee = await _employeeRepository.GetByIdAsync(id);
  151.         if (employee == null)
  152.         {
  153.             throw new AppServiceException(404, "职工不存在");
  154.         }
  155.         employee.IsActive = isActive;
  156.         await _employeeRepository.UpdateAsync(employee);
  157.     }
  158.     /// <summary>
  159.     /// 转移职工到新部门
  160.     /// </summary>
  161.     public async Task TransferEmployeeAsync(long employeeId, long? newDepartmentId)
  162.     {
  163.         var employee = await _employeeRepository.GetByIdAsync(employeeId);
  164.         if (employee == null)
  165.         {
  166.             throw new AppServiceException(404, "职工不存在");
  167.         }
  168.         if (newDepartmentId.HasValue)
  169.         {
  170.             var departmentExists = await _departmentRepository.ExistsAsync(d => d.Id == newDepartmentId.Value);
  171.             if (!departmentExists)
  172.             {
  173.                 throw new AppServiceException(400, "部门不存在");
  174.             }
  175.         }
  176.         employee.DepartmentId = newDepartmentId;
  177.         await _employeeRepository.UpdateAsync(employee);
  178.     }
  179.     /// <summary>
  180.     /// 办理职工离职
  181.     /// </summary>
  182.     public async Task TerminateEmployeeAsync(long employeeId, DateTime terminationDate)
  183.     {
  184.         var employee = await _employeeRepository.GetByIdAsync(employeeId);
  185.         if (employee == null)
  186.         {
  187.             throw new AppServiceException(404, "职工不存在");
  188.         }
  189.         employee.EmploymentStatus = EmploymentStatus.Resigned;
  190.         employee.TerminationDate = terminationDate;
  191.         employee.IsActive = false;
  192.         
  193.         await _employeeRepository.UpdateAsync(employee);
  194.     }
  195.     /// <summary>
  196.     /// 验证工号是否唯一
  197.     /// </summary>
  198.     public async Task<bool> IsEmployeeNoUniqueAsync(string employeeNo, long? excludeId = null)
  199.     {
  200.         var query = _employeeRepository.CreateQuery()
  201.             .Where(e => e.EmployeeNo == employeeNo && e.TenantId == _currentUser.TenantId);
  202.         if (excludeId.HasValue)
  203.         {
  204.             query = query.Where(e => e.Id != excludeId.Value);
  205.         }
  206.         return !await query.AnyAsync();
  207.     }
  208.     /// <summary>
  209.     /// 验证创建DTO
  210.     /// </summary>
  211.     protected override async Task ValidateCreateDto(CreateEmployeeDto createDto)
  212.     {
  213.         await base.ValidateCreateDto(createDto);
  214.         // 验证工号唯一性
  215.         bool isUnique = await IsEmployeeNoUniqueAsync(createDto.EmployeeNo);
  216.         if (!isUnique)
  217.         {
  218.             throw new AppServiceException(400, $"工号 {createDto.EmployeeNo} 已存在,请使用其他工号");
  219.         }
  220.         // 验证部门是否存在
  221.         if (createDto.DepartmentId.HasValue)
  222.         {
  223.             var departmentExists = await _departmentRepository.ExistsAsync(d => d.Id == createDto.DepartmentId.Value);
  224.             if (!departmentExists)
  225.             {
  226.                 throw new AppServiceException(400, "部门不存在");
  227.             }
  228.         }
  229.         // 验证用户是否存在(如果指定了用户ID)
  230.         if (createDto.UserId.HasValue)
  231.         {
  232.             var userExists = await _userRepository.ExistsAsync(u => u.Id == createDto.UserId.Value);
  233.             if (!userExists)
  234.             {
  235.                 throw new AppServiceException(400, "用户不存在");
  236.             }
  237.         }
  238.     }
  239.     /// <summary>
  240.     /// 验证更新DTO
  241.     /// </summary>
  242.     protected override async Task ValidateUpdateDto(long id, UpdateEmployeeDto updateDto)
  243.     {
  244.         await base.ValidateUpdateDto(id, updateDto);
  245.         // 验证工号唯一性(排除当前记录)
  246.         bool isUnique = await IsEmployeeNoUniqueAsync(updateDto.EmployeeNo, id);
  247.         if (!isUnique)
  248.         {
  249.             throw new AppServiceException(400, $"工号 {updateDto.EmployeeNo} 已存在,请使用其他工号");
  250.         }
  251.         // 验证部门是否存在
  252.         if (updateDto.DepartmentId.HasValue)
  253.         {
  254.             var departmentExists = await _departmentRepository.ExistsAsync(d => d.Id == updateDto.DepartmentId.Value);
  255.             if (!departmentExists)
  256.             {
  257.                 throw new AppServiceException(400, "部门不存在");
  258.             }
  259.         }
  260.         // 验证用户是否存在(如果指定了用户ID)
  261.         if (updateDto.UserId.HasValue)
  262.         {
  263.             var userExists = await _userRepository.ExistsAsync(u => u.Id == updateDto.UserId.Value);
  264.             if (!userExists)
  265.             {
  266.                 throw new AppServiceException(400, "用户不存在");
  267.             }
  268.         }
  269.     }
  270.     /// <summary>
  271.     /// 创建实体前的处理
  272.     /// </summary>
  273.     protected override async Task<Employee> OnCreating(CreateEmployeeDto createDto)
  274.     {
  275.         var employee = await base.OnCreating(createDto);
  276.         
  277.         // 设置租户ID
  278.         employee.TenantId = _currentUser.TenantId;
  279.         
  280.         // 生成ID(如果需要)
  281.         if (employee.Id == 0)
  282.         {
  283.             employee.Id = await _idGenerator.GenerateIdAsync();
  284.         }
  285.         return employee;
  286.     }
  287. }
复制代码
说明

  • 继承自BaseCRUDIService,自动获得标准的CRUD方法和批量导入功能
  • 实现IScopedDependency接口,服务会自动注册
  • 重写ValidateCreateDto和ValidateUpdateDto方法实现业务验证(工号唯一性、部门存在性等)
  • 重写OnCreating方法设置租户ID和生成ID
  • 使用LinqKit的PredicateBuilder构建动态查询条件
  • 提供额外的业务方法(设置激活状态、转移部门、办理离职等)
5. 创建控制器

在Controllers目录下创建控制器:
  1. // Controllers/EmployeesController.cs
  2. using CodeSpirit.Core;
  3. using CodeSpirit.Core.Attributes;
  4. using CodeSpirit.Core.Dtos;
  5. using CodeSpirit.Core.Enums;
  6. using CodeSpirit.IdentityApi.Dtos.Employee;
  7. using CodeSpirit.IdentityApi.Services;
  8. using CodeSpirit.Shared.Dtos.Common;
  9. using Microsoft.AspNetCore.Mvc;
  10. using System.ComponentModel;
  11. namespace CodeSpirit.IdentityApi.Controllers;
  12. /// <summary>
  13. /// 职工管理控制器
  14. /// </summary>
  15. [DisplayName("职工管理")]
  16. [Navigation(Icon = "fa-solid fa-user-tie", PlatformType = PlatformType.Tenant)]
  17. public class EmployeesController : ApiControllerBase
  18. {
  19.     private readonly IEmployeeService _employeeService;
  20.     /// <summary>
  21.     /// 构造函数
  22.     /// </summary>
  23.     public EmployeesController(IEmployeeService employeeService)
  24.     {
  25.         _employeeService = employeeService;
  26.     }
  27.     /// <summary>
  28.     /// 获取职工列表
  29.     /// </summary>
  30.     /// <param name="queryDto">查询条件</param>
  31.     /// <returns>职工列表结果</returns>
  32.     [HttpGet]
  33.     [DisplayName("获取职工列表")]
  34.     public async Task>>> GetEmployees([FromQuery] EmployeeQueryDto queryDto)
  35.     {
  36.         var employees = await _employeeService.GetEmployeesAsync(queryDto);
  37.         return SuccessResponse(employees);
  38.     }
  39.     /// <summary>
  40.     /// 根据部门获取职工列表
  41.     /// </summary>
  42.     /// <param name="departmentId">部门ID</param>
  43.     /// <param name="includeSubDepartments">是否包含子部门</param>
  44.     /// <returns>职工列表</returns>
  45.     [HttpGet("department/{departmentId}")]
  46.     [DisplayName("根据部门获取职工")]
  47.     public async Task>>> GetEmployeesByDepartment(
  48.         long departmentId,
  49.         [FromQuery] bool includeSubDepartments = false)
  50.     {
  51.         var employees = await _employeeService.GetEmployeesByDepartmentAsync(departmentId, includeSubDepartments);
  52.         return SuccessResponse(employees);
  53.     }
  54.     /// <summary>
  55.     /// 获取职工详情
  56.     /// </summary>
  57.     /// <param name="id">职工ID</param>
  58.     /// <returns>职工详细信息</returns>
  59.     [HttpGet("{id:long}")]
  60.     [DisplayName("获取职工详情")]
  61.     public async Task>> GetEmployee(long id)
  62.     {
  63.         var employee = await _employeeService.GetAsync(id);
  64.         return SuccessResponse(employee);
  65.     }
  66.     /// <summary>
  67.     /// 创建职工
  68.     /// </summary>
  69.     /// <param name="createDto">创建职工请求数据</param>
  70.     /// <returns>创建的职工信息</returns>
  71.     [HttpPost]
  72.     [DisplayName("创建职工")]
  73.     public async Task>> CreateEmployee(CreateEmployeeDto createDto)
  74.     {
  75.         ArgumentNullException.ThrowIfNull(createDto);
  76.         var employeeDto = await _employeeService.CreateAsync(createDto);
  77.         return SuccessResponse(employeeDto);
  78.     }
  79.     /// <summary>
  80.     /// 更新职工
  81.     /// </summary>
  82.     /// <param name="id">职工ID</param>
  83.     /// <param name="updateDto">更新职工请求数据</param>
  84.     /// <returns>更新操作结果</returns>
  85.     [HttpPut("{id:long}")]
  86.     [DisplayName("更新职工")]
  87.     public async Task> UpdateEmployee(long id, UpdateEmployeeDto updateDto)
  88.     {
  89.         await _employeeService.UpdateAsync(id, updateDto);
  90.         return SuccessResponse();
  91.     }
  92.     /// <summary>
  93.     /// 删除职工
  94.     /// </summary>
  95.     /// <param name="id">职工ID</param>
  96.     /// <returns>删除操作结果</returns>
  97.     [HttpDelete("{id:long}")]
  98.     [Operation("删除", "ajax", null, "确定要删除此职工吗?")]
  99.     [DisplayName("删除职工")]
  100.     public async Task> DeleteEmployee(long id)
  101.     {
  102.         await _employeeService.DeleteAsync(id);
  103.         return SuccessResponse();
  104.     }
  105.     /// <summary>
  106.     /// 批量删除职工
  107.     /// </summary>
  108.     /// <param name="request">批量删除请求</param>
  109.     /// <returns>批量删除操作结果</returns>
  110.     [HttpPost("batch-delete")]
  111.     [Operation("批量删除", "ajax", null, "确定要批量删除选中的职工吗?", isBulkOperation: true)]
  112.     [DisplayName("批量删除职工")]
  113.     public async Task> BatchDeleteEmployees([FromBody] BatchOperationDto<long> request)
  114.     {
  115.         ArgumentNullException.ThrowIfNull(request);
  116.         (int successCount, List<long> failedIds) = await _employeeService.BatchDeleteAsync(request.Ids);
  117.         
  118.         return failedIds.Any()
  119.             ? SuccessResponse($"成功删除 {successCount} 个职工,但以下职工删除失败: {string.Join(", ", failedIds)}")
  120.             : SuccessResponse($"成功删除 {successCount} 个职工!");
  121.     }
  122.     /// <summary>
  123.     /// 设置职工激活状态
  124.     /// </summary>
  125.     /// <param name="id">职工ID</param>
  126.     /// <param name="isActive">是否激活</param>
  127.     /// <returns>操作结果</returns>
  128.     [HttpPut("{id:long}/active")]
  129.     [DisplayName("设置激活状态")]
  130.     public async Task> SetActiveStatus(long id, [FromBody] bool isActive)
  131.     {
  132.         await _employeeService.SetActiveStatusAsync(id, isActive);
  133.         return SuccessResponse();
  134.     }
  135.     /// <summary>
  136.     /// 转移职工到新部门
  137.     /// </summary>
  138.     /// <param name="id">职工ID</param>
  139.     /// <param name="request">转移请求</param>
  140.     /// <returns>操作结果</returns>
  141.     [HttpPut("{id:long}/transfer")]
  142.     [DisplayName("转移部门")]
  143.     public async Task> TransferEmployee(long id, [FromBody] TransferEmployeeRequest request)
  144.     {
  145.         await _employeeService.TransferEmployeeAsync(id, request.DepartmentId);
  146.         return SuccessResponse();
  147.     }
  148.     /// <summary>
  149.     /// 办理职工离职
  150.     /// </summary>
  151.     /// <param name="id">职工ID</param>
  152.     /// <param name="request">离职请求</param>
  153.     /// <returns>操作结果</returns>
  154.     [HttpPut("{id:long}/terminate")]
  155.     [DisplayName("办理离职")]
  156.     public async Task> TerminateEmployee(long id, [FromBody] TerminateEmployeeRequest request)
  157.     {
  158.         await _employeeService.TerminateEmployeeAsync(id, request.TerminationDate);
  159.         return SuccessResponse();
  160.     }
  161. }
  162. /// <summary>
  163. /// 转移职工请求
  164. /// </summary>
  165. public class TransferEmployeeRequest
  166. {
  167.     public long? DepartmentId { get; set; }
  168. }
  169. /// <summary>
  170. /// 离职请求
  171. /// </summary>
  172. public class TerminateEmployeeRequest
  173. {
  174.     public DateTime TerminationDate { get; set; }
  175. }
复制代码
说明

  • 继承自ApiControllerBase,自动获得统一的响应格式和异常处理
  • DisplayName特性用于前端界面显示
  • Navigation特性用于添加到导航菜单
  • Operation特性用于配置操作按钮(删除确认对话框)
  • 使用SuccessResponse方法返回统一的成功响应
  • 提供额外的业务操作接口(设置激活状态、转移部门、办理离职等)
6. 配置数据库上下文

在Data目录下的DbContext中添加实体:
  1. // Data/ApplicationDbContext.cs
  2. using CodeSpirit.IdentityApi.Data.Models;
  3. using CodeSpirit.Shared.Data;
  4. using Microsoft.EntityFrameworkCore;
  5. namespace CodeSpirit.IdentityApi.Data;
  6. /// <summary>
  7. /// 身份认证系统数据库上下文 - 支持多租户和多数据库
  8. /// </summary>
  9. public class ApplicationDbContext : MultiDatabaseDbContextBase
  10. {
  11.     /// <summary>
  12.     /// 职工
  13.     /// </summary>
  14.     public DbSet<Employee> Employees => Set<Employee>();
  15.     protected override void OnModelCreating(ModelBuilder modelBuilder)
  16.     {
  17.         base.OnModelCreating(modelBuilder);
  18.         // 配置Employee实体
  19.         modelBuilder.Entity<Employee>(entity =>
  20.         {
  21.             entity.ToTable(nameof(Employee));
  22.             entity.Property(e => e.Id).ValueGeneratedNever();
  23.             // 租户感知的工号复合唯一索引:同一租户内工号唯一
  24.             entity.HasIndex(e => new { e.TenantId, e.EmployeeNo })
  25.                 .IsUnique()
  26.                 .HasDatabaseName("IX_Employee_TenantId_EmployeeNo");
  27.             // 索引 DepartmentId,提高查询部门员工的性能
  28.             entity.HasIndex(e => e.DepartmentId)
  29.                 .HasDatabaseName("IX_Employee_DepartmentId");
  30.             // 索引 UserId,提高查询用户关联的性能
  31.             entity.HasIndex(e => e.UserId)
  32.                 .HasDatabaseName("IX_Employee_UserId");
  33.             // 索引 IsActive,提高按状态过滤的性能
  34.             entity.HasIndex(e => e.IsActive)
  35.                 .HasDatabaseName("IX_Employee_IsActive");
  36.             // 索引 EmploymentStatus,提高按在职状态过滤的性能
  37.             entity.HasIndex(e => e.EmploymentStatus)
  38.                 .HasDatabaseName("IX_Employee_EmploymentStatus");
  39.             // 配置与部门的关系
  40.             entity.HasOne(e => e.Department)
  41.                 .WithMany()
  42.                 .HasForeignKey(e => e.DepartmentId)
  43.                 .OnDelete(DeleteBehavior.SetNull);
  44.             // 配置与用户的关系
  45.             entity.HasOne(e => e.User)
  46.                 .WithMany()
  47.                 .HasForeignKey(e => e.UserId)
  48.                 .OnDelete(DeleteBehavior.SetNull);
  49.         });
  50.     }
  51. }
复制代码
说明

  • 继承自MultiDatabaseDbContextBase,支持MySQL和SQL Server
  • 配置表名、主键、字段长度等
  • 配置复合唯一索引(租户ID + 工号),确保同一租户内工号唯一
  • 配置关联关系的级联删除策略(SetNull表示删除部门或用户时,职工记录保留但关联字段设为null)
  • 添加必要的索引提升查询性能
7. 服务注册

CodeSpirit框架通过标记接口自动注册服务,无需手动注册:
  1. // IEmployeeService接口继承了IScopedDependency接口
  2. public interface IEmployeeService : IBaseCRUDIService<...>, IScopedDependency
  3. {
  4.     // ...
  5. }
复制代码
说明

  • 服务接口继承IScopedDependency接口,服务会自动注册为Scoped生命周期
  • 框架会自动扫描并注册所有标记接口的服务
  • 无需在Program.cs中手动注册
8. 创建数据库迁移

CodeSpirit框架支持多数据库架构,迁移文件按数据库类型分离存储。创建迁移时必须指定迁移目录参数。
  1. # 进入IdentityApi项目目录
  2. cd Src/ApiServices/CodeSpirit.IdentityApi
  3. # 创建迁移(根据数据库类型选择)
  4. # MySQL - 迁移文件将保存到 Migrations/MySql/ 目录
  5. dotnet ef migrations add AddEmployees --context MySqlApplicationDbContext --output-dir Migrations/MySql
  6. # SQL Server - 迁移文件将保存到 Migrations/SqlServer/ 目录
  7. dotnet ef migrations add AddEmployees --context SqlServerApplicationDbContext --output-dir Migrations/SqlServer
  8. # 应用迁移
  9. dotnet ef database update --context MySqlApplicationDbContext
  10. # 或
  11. dotnet ef database update --context SqlServerApplicationDbContext
复制代码
迁移目录结构
  1. Src/ApiServices/CodeSpirit.IdentityApi/
  2. ├── Migrations/
  3. │   ├── MySql/                          # MySQL迁移文件
  4. │   │   ├── 20251222_AddEmployees.cs
  5. │   │   ├── 20251222_AddEmployees.Designer.cs
  6. │   │   └── MySqlApplicationDbContextModelSnapshot.cs
  7. │   └── SqlServer/                      # SQL Server迁移文件
  8. │       ├── 20251222_AddEmployees.cs
  9. │       ├── 20251222_AddEmployees.Designer.cs
  10. │       └── 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办理职工离职业务验证示例

创建时验证
  1. protected override async Task ValidateCreateDto(CreateEmployeeDto createDto)
  2. {
  3.     await base.ValidateCreateDto(createDto);
  4.     // 验证工号唯一性
  5.     bool isUnique = await IsEmployeeNoUniqueAsync(createDto.EmployeeNo);
  6.     if (!isUnique)
  7.     {
  8.         throw new AppServiceException(400, $"工号 {createDto.EmployeeNo} 已存在,请使用其他工号");
  9.     }
  10.     // 验证部门是否存在
  11.     if (createDto.DepartmentId.HasValue)
  12.     {
  13.         var departmentExists = await _departmentRepository.ExistsAsync(d => d.Id == createDto.DepartmentId.Value);
  14.         if (!departmentExists)
  15.         {
  16.             throw new AppServiceException(400, "部门不存在");
  17.         }
  18.     }
  19.     // 验证用户是否存在(如果指定了用户ID)
  20.     if (createDto.UserId.HasValue)
  21.     {
  22.         var userExists = await _userRepository.ExistsAsync(u => u.Id == createDto.UserId.Value);
  23.         if (!userExists)
  24.         {
  25.             throw new AppServiceException(400, "用户不存在");
  26.         }
  27.     }
  28. }
复制代码
更新时验证
  1. protected override async Task ValidateUpdateDto(long id, UpdateEmployeeDto updateDto)
  2. {
  3.     await base.ValidateUpdateDto(id, updateDto);
  4.     // 验证工号唯一性(排除当前记录)
  5.     bool isUnique = await IsEmployeeNoUniqueAsync(updateDto.EmployeeNo, id);
  6.     if (!isUnique)
  7.     {
  8.         throw new AppServiceException(400, $"工号 {updateDto.EmployeeNo} 已存在,请使用其他工号");
  9.     }
  10.     // 验证部门是否存在
  11.     if (updateDto.DepartmentId.HasValue)
  12.     {
  13.         var departmentExists = await _departmentRepository.ExistsAsync(d => d.Id == updateDto.DepartmentId.Value);
  14.         if (!departmentExists)
  15.         {
  16.             throw new AppServiceException(400, "部门不存在");
  17.         }
  18.     }
  19. }
复制代码
创建前处理
  1. protected override async Task<Employee> OnCreating(CreateEmployeeDto createDto)
  2. {
  3.     var employee = await base.OnCreating(createDto);
  4.    
  5.     // 设置租户ID
  6.     employee.TenantId = _currentUser.TenantId;
  7.    
  8.     // 生成ID(如果需要)
  9.     if (employee.Id == 0)
  10.     {
  11.         employee.Id = await _idGenerator.GenerateIdAsync();
  12.     }
  13.     return employee;
  14. }
复制代码
扩展功能示例

添加权限控制
  1. [HttpPost]
  2. [DisplayName("创建职工")]
  3. [Permission("identity_employees_create")]  // 添加权限控制
  4. public async Task>> CreateEmployee(CreateEmployeeDto createDto)
  5. {
  6.     // ...
  7. }
复制代码
添加导航菜单
  1. [DisplayName("职工管理")]
  2. [Navigation(Icon = "fa-solid fa-user-tie", PlatformType = PlatformType.Tenant)]  // 添加到导航菜单
  3. public class EmployeesController : ApiControllerBase
  4. {
  5.     // ...
  6. }
复制代码
自定义查询方法
  1. /// <summary>
  2. /// 获取在职职工列表
  3. /// </summary>
  4. public async Task<List<EmployeeDto>> GetActiveEmployeesAsync()
  5. {
  6.     var employees = await Repository.CreateQuery()
  7.         .Where(e => e.IsActive && e.EmploymentStatus == EmploymentStatus.Active)
  8.         .Include(e => e.Department)
  9.         .Include(e => e.User)
  10.         .ToListAsync();
  11.     return Mapper.Map<List<EmployeeDto>>(employees);
  12. }
复制代码
使用PageAside特性实现侧边栏筛选

PageAside()特性用于将查询字段放置在页面侧边栏,特别适用于树形选择、分类筛选等场景。使用此特性后,该字段会从主查询表单中移除,仅在侧边栏显示。
特性说明
  1. /// <summary>
  2. /// 部门ID筛选
  3. /// </summary>
  4. [DisplayName("部门")]
  5. [AmisInputTreeField(
  6.     DataSource = "${ROOT_API}/api/identity/Departments/tree",
  7.     Multiple = false,
  8.     JoinValues = true,
  9.     ExtractValue = false,
  10.     ShowOutline = true,
  11.     LabelField = "name",
  12.     ValueField = "id",
  13.     Required = false,
  14.     Clearable = true,
  15.     SubmitOnChange = true,  // 选择后自动提交查询
  16.     HeightAuto = true,
  17.     SelectFirst = false,
  18.     InputOnly = true,
  19.     ShowIcon = true
  20. )]
  21. [PageAside()]  // 标记为侧边栏字段
  22. public long? DepartmentId { get; set; }
复制代码
3.png

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文档注释
    • 使用、、标签

相关文档


  • CodeSpirit.Core核心框架
  • 开发环境搭建指南
  • 项目整体架构设计
  • 统一异常处理指南
总结

通过CodeSpirit框架的BaseCRUDIService和标准开发模式,您可以快速开发出功能完整的CRUD接口。职工管理模块展示了:

  • ✅ 标准CRUD操作的实现
  • ✅ 关联关系管理(部门、用户账号)
  • ✅ 业务验证逻辑的编写(工号唯一性、部门存在性等)
  • ✅ 多条件查询的实现(关键字、部门、状态、日期范围等)
  • ✅ 表单分组展示的使用
  • ✅ 额外业务操作的实现(设置激活状态、转移部门、办理离职等)
  • ✅ AMIS特性的使用(表格列、表单字段、图片上传等)
框架会自动处理大部分样板代码,让您专注于业务逻辑的实现。
更多交流请关注“CodeSpirit-码灵”公众号进群!!!

祝您开发愉快!
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册