5-Dapper(与 EF Core 共存)
按业务递进顺序推进,路线:Dapper 接入 → 通用分页 + DTO 封装 → RabbitMQ 消息队列 → 部署 & 配置优化,循序渐进,贴合项目实战流程。
目标:EF 负责常规 CRUD,Dapper 负责联表、分页、复杂 SQL,二者混用,不冲突。
1. 确认 NuGet 包
DeviceRepair.Infrastructure 已提前安装 Dapper + Microsoft.EntityFrameworkCore.SqlServer,无需重复安装。
2. 改造通用 Dapper 工具类
路径:DeviceRepair.Infrastructure/Utils/DapperHelper.cs 整合连接串读取、常用查询方法,兼容现有配置:
using Dapper;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using System.Data;
namespace DeviceRepair.Infrastructure.Utils
{
public static class DapperHelper
{
private static string _connStr = string.Empty;
/// <summary>
/// 初始化连接字符串
/// </summary>
public static void Init(IConfiguration config)
{
_connStr = config.GetConnectionString("SqlServer") ?? string.Empty;
}
/// <summary>
/// 获取数据库连接
/// </summary>
private static IDbConnection GetConnection()
{
return new SqlConnection(_connStr);
}
/// <summary>
/// 单表/多表查询
/// </summary>
public static async Task<List<T>> QueryAsync<T>(string sql, object? param = null)
{
using var conn = GetConnection();
return (await conn.QueryAsync<T>(sql, param)).ToList();
}
/// <summary>
/// 执行增删改SQL
/// </summary>
public static async Task<int> ExecuteAsync(string sql, object? param = null)
{
using var conn = GetConnection();
return await conn.ExecuteAsync(sql, param);
}
}
}
3. 初始化 Dapper(统一入口)
打开 ServiceCollectionExtensions.cs,在 AddDbAndRepositories 方法末尾添加初始化:
public static void AddDbAndRepositories(this IServiceCollection services, IConfiguration config)
{
string connStr = config.GetConnectionString("SqlServer")!;
services.AddDbContext<AppDbContext>(opt =>
{
opt.UseSqlServer(connStr);
});
// 注册全部仓储
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IBaseRepository<Device>, DeviceRepository>();
services.AddScoped<IBaseRepository<RepairOrder>, RepairOrderRepository>();
services.AddScoped<IBaseRepository<SystemMessage>, SystemMessageRepository>();
// 初始化 Dapper 连接串
DapperHelper.Init(config);
}
4. 扩展仓储:新增 Dapper 专属接口
4.1 新建分页接口,针对工单做联表查询(工单 + 提交人 + 设备),作为复杂查询示例:
using DeviceRepair.Core.Dtos;
namespace DeviceRepair.Core.Interfaces
{
public interface IOrderDapperRepository
{
/// <summary>
/// 工单联表分页查询
/// </summary>
Task<PageResult<RepairOrderDto>> GetOrderPageListAsync(PageQuery dto);
}
}
4.2 新建 Dapper 实体 / 查询 DTO
先提前创建通用分页 DTO
namespace DeviceRepair.Core.Dtos
{
/// <summary>
/// 分页入参
/// </summary>
//用途:前端做分页列表时,传给后端的参数。
public class PageQuery
{
public int PageIndex { get; set; } = 1; // 当前页码,默认第1页
public int PageSize { get; set; } = 10; // 每页几条数据,默认每页10条
}
/// <summary>
/// 分页出参
/// </summary>
/// <typeparam name="T">数据实体</typeparam>
//作用:后端查完数据,把「总条数、当前页、每页大小、数据列表」打包一起返回给前端,前端就能渲染分页控件 + 列表。
public class PageResult<T>
{
public int Total { get; set; } // 数据总条数
public int PageIndex { get; set; } // 当前页码
public int PageSize { get; set; } // 每页条数
public List<T> Data { get; set; } = new List<T>(); // 当前页的数据列表
}
/// <summary>
/// 工单联表展示DTO,数据库里工单表、用户表、设备表是分开的,联表查询后,不能直接返回数据库实体(字段冗余、不安全),所以单独建这个类。
/// </summary>
public class RepairOrderDto
{//专门存放「多张表联查后」要展示给前端的字段,只保留页面需要的内容。
public int Id { get; set; }
public string UserName { get; set; } = string.Empty; // 提交工单的用户名
public string DeviceName { get; set; } = string.Empty; // 故障设备名
public string ProblemDesc { get; set; } = string.Empty;// 故障描述
public int OrderStatus { get; set; } // 工单状态码
public DateTime CreateTime { get; set; } // 工单创建时间
}
}
4.3 Dapper 仓储实现
路径:DeviceRepair.Infrastructure/Repositories/OrderDapperRepository.cs
using DeviceRepair.Core.Dtos;
using DeviceRepair.Core.Interfaces;
using DeviceRepair.Infrastructure.Utils;
namespace DeviceRepair.Infrastructure.Repositories
{
public class OrderDapperRepository : IOrderDapperRepository
{
public async Task<PageResult<RepairOrderDto>> GetOrderPageListAsync(PageQuery dto)
{
var pageResult = new PageResult<RepairOrderDto>
{
PageIndex = dto.PageIndex,
PageSize = dto.PageSize
};
// 1. 查询总条数
string countSql = @"
SELECT COUNT(1)
FROM RepairOrders o
LEFT JOIN Users u ON o.UserId = u.Id
LEFT JOIN Devices d ON o.DeviceId = d.Id
WHERE o.IsDeleted = 0";
pageResult.Total = await DapperHelper.ExecuteAsync(countSql);
// 2. 分页联表查询
int skip = (dto.PageIndex - 1) * dto.PageSize;
string dataSql = $@"
SELECT o.Id, u.UserName, d.DeviceName, o.ProblemDesc, o.OrderStatus, o.CreateTime
FROM RepairOrders o
LEFT JOIN Users u ON o.UserId = u.Id
LEFT JOIN Devices d ON o.DeviceId = d.Id
WHERE o.IsDeleted = 0
ORDER BY o.CreateTime DESC
OFFSET {skip} ROWS FETCH NEXT {dto.PageSize} ROWS ONLY";
pageResult.Data = await DapperHelper.QueryAsync<RepairOrderDto>(dataSql);
return pageResult;
}
}
}
int skip = (dto.PageIndex - 1) * dto.PageSize;
- 第 1 页:
(1-1)*10 = 0→ 跳过 0 条,取前 10 条- 第 2 页:
(2-1)*10 = 10→ 跳过前 10 条,取接下来 10 条- 第 3 页:
(3-1)*10 = 20→ 跳过前 20 条
skip= 要跳过多少条数据。
4.4 注册 Dapper 仓储
ServiceCollectionExtensions.cs 追加注册:
// Dapper 专属仓储
services.AddScoped<IOrderDapperRepository, OrderDapperRepository>();
5. 新增分页接口(控制器测试)
打开 RepairOrderController.cs,添加分页接口:
private readonly IBaseRepository<RepairOrder> _orderRepo;
// 注入Dapper仓储
private readonly IOrderDapperRepository _orderDapperRepo;
public RepairOrderController(IBaseRepository<RepairOrder> orderRepo, IOrderDapperRepository orderDapperRepo)
{
_orderRepo = orderRepo;
_orderDapperRepo = orderDapperRepo;
}
/// <summary>
/// 工单联表分页查询(Dapper实现)
/// </summary>
[HttpGet("page")]
public async Task<ActionResult<ResultModel<PageResult<RepairOrderDto>>>> GetPageList([FromQuery] PageQuery dto)
{
var data = await _orderDapperRepo.GetOrderPageListAsync(dto);
return Ok(ResultModel<PageResult<RepairOrderDto>>.Success(data));
}
6. 测试
重启项目,Swagger 调用 /api/v1/RepairOrder/page 接口,传入分页参数,验证联表 + 分页数据正常返回。
阶段二:通用分页 & DTO 规范化封装
基于上一阶段的分页基础,统一入参 / 出参 DTO 规范、全局分页逻辑、实体与 DTO 映射规则,解决实体直接返回前端的安全问题。
- 统一区分:InputDto(入参)、OutputDto(出参),禁止实体直接对外暴露
- 封装通用分页基类,全模块复用
- 简单映射(手动映射,后续可扩展 AutoMapper)
- 改造现有接口,全部使用 DTO 交互
核心目标:隔离数据库实体、统一入参 / 出参规范、通用分页复用、规范字段映射,禁止直接把实体返回前端,兼顾安全性与可维护性。
一、目录结构调整(统一 DTO 分层)
在 DeviceRepair.Core/Dtos 下拆分目录,区分入参、出参、通用模型:
DeviceRepair.Core
├─ Entities
├─ Enums
├─ Interfaces
├─ Dtos
│ ├─ Common # 通用模型(分页、全局请求/响应)
│ ├─ Input # 接口入参 DTO
│ └─ Output # 接口出参 DTO
└─ Utils
二、封装通用基础模型(Common)
1. 通用分页模型(全局复用)
namespace DeviceRepair.Core.Dtos.Common
{
/// <summary>
/// 分页查询入参(所有分页接口统一使用)
/// </summary>
public class PageQuery
{
/// <summary>
/// 页码,默认第1页
/// </summary>
public int PageIndex { get; set; } = 1;
/// <summary>
/// 页大小,默认10条
/// </summary>
public int PageSize { get; set; } = 10;
}
/// <summary>
/// 分页结果出参(所有分页接口统一返回)
/// </summary>
/// <typeparam name="T">列表项实体/DTO</typeparam>
public class PageResult<T>
{
/// <summary>
/// 总数据条数
/// </summary>
public int Total { get; set; }
/// <summary>
/// 当前页码
/// </summary>
public int PageIndex { get; set; }
/// <summary>
/// 每页条数
/// </summary>
public int PageSize { get; set; }
/// <summary>
/// 数据列表
/// </summary>
public List<T> Data { get; set; } = new List<T>();
}
}
2. 全局基础 DTO(可选,后续扩展查询条件可用)
新建 Dtos/Common/BaseQuery.cs
namespace DeviceRepair.Core.Dtos.Common
{
/// <summary>
/// 通用查询基类(可扩展关键字搜索、时间范围等)
/// </summary>
public class BaseQuery : PageQuery
{
/// <summary>
/// 关键字模糊查询
/// </summary>
public string? Keyword { get; set; }
}
}
三、按模块拆分 入参 DTO(Input)
把原有接口实体传参、零散 DTO 全部迁移至 Input,禁止控制器直接接收 Entity。
1. 账号模块 Input
新建 Dtos/Input/Account/LoginInput.cs
namespace DeviceRepair.Core.Dtos.Input.Account
{
/// <summary>
/// 登录入参
/// </summary>
public class LoginInput
{
public string UserName { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
}
2. 设备模块 Input
新建 Dtos/Input/Device/DeviceInput.cs
using System.ComponentModel.DataAnnotations;
namespace DeviceRepair.Core.Dtos.Input.Device
{
/// <summary>
/// 新增/编辑设备入参
/// </summary>
public class DeviceInput
{
public int Id { get; set; }
[Required(ErrorMessage = "设备名称不能为空")]
[MaxLength(100, ErrorMessage = "设备名称最长100字符")]
public string DeviceName { get; set; } = string.Empty;
[MaxLength(50)]
public string? DeviceType { get; set; }
[MaxLength(100)]
public string? Location { get; set; }
}
}
3. 工单模块 Input
新建 Dtos/Input/RepairOrder/OrderInput.cs
using System.ComponentModel.DataAnnotations;
namespace DeviceRepair.Core.Dtos.Input.RepairOrder
{
/// <summary>
/// 新增/编辑工单入参
/// </summary>
public class OrderInput
{
public int Id { get; set; }
[Required(ErrorMessage = "请选择用户")]
public int UserId { get; set; }
[Required(ErrorMessage = "请选择设备")]
public int DeviceId { get; set; }
[Required(ErrorMessage = "问题描述不能为空")]
[MaxLength(500)]
public string ProblemDesc { get; set; } = string.Empty;
public int? RepairUserId { get; set; }
public string? RepairRemark { get; set; }
}
}
4. 消息模块 Input
新建 Dtos/Input/Message/MessageInput.cs
using System.ComponentModel.DataAnnotations;
namespace DeviceRepair.Core.Dtos.Input.Message
{
/// <summary>
/// 新增/编辑消息入参
/// </summary>
public class MessageInput
{
public int Id { get; set; }
[Required]
public int ReceiveUserId { get; set; }
[Required]
[MaxLength(200)]
public string Content { get; set; } = string.Empty;
}
}
四、按模块拆分 出参 DTO(Output)
创建对外输出模型,过滤敏感字段(如密码)、精简冗余字段,前端只拿需要的数据。
1. 账号模块 Output
新建 Dtos/Output/Account/UserOutput.cs
using DeviceRepair.Core.Enums;
namespace DeviceRepair.Core.Dtos.Output.Account
{
/// <summary>
/// 用户信息出参(屏蔽密码)
/// </summary>
public class UserOutput
{
public int Id { get; set; }
public string UserName { get; set; } = string.Empty;
public string? RealName { get; set; }
public UserRole Role { get; set; }
public DateTime CreateTime { get; set; }
}
}
2. 设备模块 Output
新建 Dtos/Output/Device/DeviceOutput.cs
namespace DeviceRepair.Core.Dtos.Output.Device
{
/// <summary>
/// 设备信息出参
/// </summary>
public class DeviceOutput
{
public int Id { get; set; }
public string DeviceName { get; set; } = string.Empty;
public string? DeviceType { get; set; }
public string? Location { get; set; }
}
}
3. 工单模块 Output(含联表分页 DTO)
新建 Dtos/Output/RepairOrder/OrderOutput.cs
namespace DeviceRepair.Core.Dtos.Output.RepairOrder
{
/// <summary>
/// 工单基础信息出参
/// </summary>
public class OrderOutput
{
public int Id { get; set; }
public int UserId { get; set; }
public int DeviceId { get; set; }
public string ProblemDesc { get; set; } = string.Empty;
public int OrderStatus { get; set; }
public int? RepairUserId { get; set; }
public string? RepairRemark { get; set; }
public DateTime CreateTime { get; set; }
public DateTime? FinishTime { get; set; }
}
/// <summary>
/// 工单联表分页出参(Dapper 专用)
/// </summary>
public class OrderPageOutput
{
public int Id { get; set; }
public string UserName { get; set; } = string.Empty;
public string DeviceName { get; set; } = string.Empty;
public string ProblemDesc { get; set; } = string.Empty;
public int OrderStatus { get; set; }
public DateTime CreateTime { get; set; }
}
}
4. 消息模块 Output
新建 Dtos/Output/Message/MessageOutput.cs
namespace DeviceRepair.Core.Dtos.Output.Message
{
/// <summary>
/// 系统消息出参
/// </summary>
public class MessageOutput
{
public int Id { get; set; }
public int ReceiveUserId { get; set; }
public string Content { get; set; } = string.Empty;
public bool IsRead { get; set; }
public DateTime CreateTime { get; set; }
}
}
五、统一实体 ↔ DTO 映射规则
现阶段使用手动映射(轻量化,无第三方组件),约定映射位置:
- Controller 层:简单字段映射(单实体)
- Repository/Service 层:复杂联表、分页映射
- 禁止在视图 / 前端直接使用数据库实体
扩展:可复用映射静态类(可选)
新建 DeviceRepair.Core/Utils/ModelMapper.cs,集中写映射方法,全局调用
using DeviceRepair.Core.Entities;
using DeviceRepair.Core.Dtos.Input.Device;
using DeviceRepair.Core.Dtos.Input.RepairOrder;
using DeviceRepair.Core.Dtos.Input.Message;
using DeviceRepair.Core.Dtos.Output.Account;
using DeviceRepair.Core.Dtos.Output.Device;
using DeviceRepair.Core.Dtos.Output.RepairOrder;
using DeviceRepair.Core.Dtos.Output.Message;
namespace DeviceRepair.Core.Utils
{
/// <summary>
/// 实体与DTO手动映射工具
/// </summary>
public static class ModelMapper
{
#region 用户映射
public static UserOutput ToOutput(this User entity)
{
return new UserOutput
{
Id = entity.Id,
UserName = entity.UserName,
RealName = entity.RealName,
Role = entity.Role,
CreateTime = entity.CreateTime
};
}
#endregion
#region 设备映射
public static Device ToEntity(this DeviceInput dto)
{
return new Device
{
Id = dto.Id,
DeviceName = dto.DeviceName,
DeviceType = dto.DeviceType,
Location = dto.Location
};
}
public static DeviceOutput ToOutput(this Device entity)
{
return new DeviceOutput
{
Id = entity.Id,
DeviceName = entity.DeviceName,
DeviceType = entity.DeviceType,
Location = entity.Location
};
}
#endregion
#region 工单映射
public static RepairOrder ToEntity(this OrderInput dto)
{
return new RepairOrder
{
Id = dto.Id,
UserId = dto.UserId,
DeviceId = dto.DeviceId,
ProblemDesc = dto.ProblemDesc,
RepairUserId = dto.RepairUserId,
RepairRemark = dto.RepairRemark
};
}
public static OrderOutput ToOutput(this RepairOrder entity)
{
return new OrderOutput
{
Id = entity.Id,
UserId = entity.UserId,
DeviceId = entity.DeviceId,
ProblemDesc = entity.ProblemDesc,
OrderStatus = entity.OrderStatus,
RepairUserId = entity.RepairUserId,
RepairRemark = entity.RepairRemark,
CreateTime = entity.CreateTime,
FinishTime = entity.FinishTime
};
}
#endregion
#region 消息映射
public static SystemMessage ToEntity(this MessageInput dto)
{
return new SystemMessage
{
Id = dto.Id,
ReceiveUserId = dto.ReceiveUserId,
Content = dto.Content
};
}
public static MessageOutput ToOutput(this SystemMessage entity)
{
return new MessageOutput
{
Id = entity.Id,
ReceiveUserId = entity.ReceiveUserId,
Content = entity.Content,
IsRead = entity.IsRead,
CreateTime = entity.CreateTime
};
}
#endregion
}
}
-
你现在这种:手动映射(当前代码)
- 优点:无第三方依赖、灵活、可控、排查问题简单,小 / 中型项目常用。
- 缺点:字段多了会写很多重复赋值代码。
-
行业常用进阶:AutoMapper 自动映射 第三方库,配置一行代码就能自动拷贝同名字段,不用手写
A.XX=B.XX。 你现在是手动版映射,是学习基础。
六、改造原有接口:全部切换为 DTO 交互
逐个修改控制器,替换入参、出参,使用映射方法,删除直接返回实体的写法。
1. AccountController 改造
using DeviceRepair.Core;
using DeviceRepair.Core.Dtos.Input.Account;
using DeviceRepair.Core.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DeviceRepair.Api.Controllers
{
[Route("api/v1/[controller]")]
[ApiController]
public class AccountController : ControllerBase
{
private readonly IAccountService _accountService;
public AccountController(IAccountService accountService)
{
_accountService = accountService;
}
/// <summary>
/// 登录
/// </summary>
[HttpPost("login")]
[AllowAnonymous]
public async Task<ActionResult<ResultModel<string>>> Login([FromBody] LoginInput dto)
{
var result = await _accountService.LoginAsync(dto);
return Ok(result);
}
}
}
同步修改 IAccountService 和 AccountService 入参:
IAccountService.cs
using DeviceRepair.Core;
using DeviceRepair.Core.Dtos.Input.Account;
namespace DeviceRepair.Core.Interfaces
{
public interface IAccountService
{
Task<ResultModel<string>> LoginAsync(LoginInput dto);
}
}
AccountService.cs
using DeviceRepair.Core;
using DeviceRepair.Core.Dtos.Input.Account;
using DeviceRepair.Core.Entities;
using DeviceRepair.Core.Interfaces;
using DeviceRepair.Core.Utils;
using Microsoft.Extensions.Configuration;
namespace DeviceRepair.Services.Modules.Account
{
public class AccountService : IAccountService
{
private readonly IUserRepository _userRepo;
private readonly IConfiguration _config;
public AccountService(IUserRepository userRepo, IConfiguration config)
{
_userRepo = userRepo;
_config = config;
}
public async Task<ResultModel<string>> LoginAsync(LoginInput dto)
{
var user = await _userRepo.CheckLoginAsync(dto.UserName, dto.Password);
if (user == null)
{
return ResultModel<string>.Fail("账号或密码错误", 401);
}
var secret = _config["Jwt:SecretKey"]!;
var expire = int.Parse(_config["Jwt:ExpireMinutes"]!);
var token = JwtHelper.GenerateToken(user, secret, expire);
return ResultModel<string>.Success(token, "登录成功");
}
}
}
2. DeviceController 改造
using DeviceRepair.Core;
using DeviceRepair.Core.Dtos.Common;
using DeviceRepair.Core.Dtos.Input.Device;
using DeviceRepair.Core.Dtos.Output.Device;
using DeviceRepair.Core.Entities;
using DeviceRepair.Core.Enums;
using DeviceRepair.Core.Interfaces;
using DeviceRepair.Core.Utils;
using DeviceRepair.Api.Filters;
using Microsoft.AspNetCore.Mvc;
namespace DeviceRepair.Api.Controllers
{
[Route("api/v1/[controller]")]
[ApiController]
public class DeviceController : ControllerBase
{
private readonly IBaseRepository<Device> _deviceRepo;
public DeviceController(IBaseRepository<Device> deviceRepo)
{
_deviceRepo = deviceRepo;
}
[HttpGet]
public async Task<ActionResult<ResultModel<List<DeviceOutput>>>> GetList()
{
var list = await _deviceRepo.GetListAsync();
var outputList = list.Select(x => x.ToOutput()).ToList();
return Ok(ResultModel<List<DeviceOutput>>.Success(outputList));
}
[HttpGet("{id}")]
public async Task<ActionResult<ResultModel<DeviceOutput?>>> GetById(int id)
{
var entity = await _deviceRepo.GetByIdAsync(id);
if (entity == null)
return Ok(ResultModel<DeviceOutput?>.Fail("数据不存在"));
return Ok(ResultModel<DeviceOutput?>.Success(entity.ToOutput()));
}
[HttpPost]
[AllowRole(UserRole.Admin)]
public async Task<ActionResult<ResultModel<bool>>> Add([FromBody] DeviceInput dto)
{
var entity = dto.ToEntity();
await _deviceRepo.AddAsync(entity);
return Ok(ResultModel<bool>.Success(true, "新增成功"));
}
[HttpPut]
[AllowRole(UserRole.Admin)]
public async Task<ActionResult<ResultModel<bool>>> Update([FromBody] DeviceInput dto)
{
var entity = dto.ToEntity();
await _deviceRepo.UpdateAsync(entity);
return Ok(ResultModel<bool>.Success(true, "修改成功"));
}
[HttpDelete("{id}")]
[AllowRole(UserRole.Admin)]
public async Task<ActionResult<ResultModel<bool>>> Delete(int id)
{
await _deviceRepo.DeleteAsync(id);
return Ok(ResultModel<bool>.Success(true, "删除成功"));
}
}
}
3. RepairOrderController 改造(含 Dapper 分页)
using DeviceRepair.Core;
using DeviceRepair.Core.Dtos.Common;
using DeviceRepair.Core.Dtos.Input.RepairOrder;
using DeviceRepair.Core.Dtos.Output.RepairOrder;
using DeviceRepair.Core.Entities;
using DeviceRepair.Core.Enums;
using DeviceRepair.Core.Interfaces;
using DeviceRepair.Core.Utils;
using DeviceRepair.Api.Filters;
using Microsoft.AspNetCore.Mvc;
namespace DeviceRepair.Api.Controllers
{
[Route("api/v1/[controller]")]
[ApiController]
public class RepairOrderController : ControllerBase
{
private readonly IBaseRepository<RepairOrder> _orderRepo;
private readonly IOrderDapperRepository _orderDapperRepo;
public RepairOrderController(IBaseRepository<RepairOrder> orderRepo, IOrderDapperRepository orderDapperRepo)
{
_orderRepo = orderRepo;
_orderDapperRepo = orderDapperRepo;
}
[HttpGet]
public async Task<ActionResult<ResultModel<List<OrderOutput>>>> GetList()
{
var list = await _orderRepo.GetListAsync();
var output = list.Select(x => x.ToOutput()).ToList();
return Ok(ResultModel<List<OrderOutput>>.Success(output));
}
[HttpGet("{id}")]
public async Task<ActionResult<ResultModel<OrderOutput?>>> GetById(int id)
{
var entity = await _orderRepo.GetByIdAsync(id);
if (entity == null)
return Ok(ResultModel<OrderOutput?>.Fail("工单不存在"));
return Ok(ResultModel<OrderOutput?>.Success(entity.ToOutput()));
}
[HttpPost]
[AllowRole(UserRole.Staff, UserRole.RepairMan)]
public async Task<ActionResult<ResultModel<bool>>> Add([FromBody] OrderInput dto)
{
var entity = dto.ToEntity();
entity.CreateTime = DateTime.Now;
entity.OrderStatus = OrderStatus.WaitAccept;
await _orderRepo.AddAsync(entity);
return Ok(ResultModel<bool>.Success(true, "工单提交成功"));
}
[HttpPut]
[AllowRole(UserRole.Admin)]
public async Task<ActionResult<ResultModel<bool>>> Update([FromBody] OrderInput dto)
{
var entity = dto.ToEntity();
await _orderRepo.UpdateAsync(entity);
return Ok(ResultModel<bool>.Success(true, "工单修改成功"));
}
[HttpDelete("{id}")]
[AllowRole(UserRole.Admin)]
public async Task<ActionResult<ResultModel<bool>>> Delete(int id)
{
await _orderRepo.DeleteAsync(id);
return Ok(ResultModel<bool>.Success(true, "工单删除成功"));
}
/// <summary>
/// Dapper 联表分页查询
/// </summary>
[HttpGet("page")]
public async Task<ActionResult<ResultModel<PageResult<OrderPageOutput>>>> GetPageList([FromQuery] PageQuery dto)
{
var pageData = await _orderDapperRepo.GetOrderPageListAsync(dto);
return Ok(ResultModel<PageResult<OrderPageOutput>>.Success(pageData));
}
}
}
同步修改 IOrderDapperRepository 和 OrderDapperRepository 中 DTO 引用:
IOrderDapperRepository.cs
using DeviceRepair.Core.Dtos.Common;
using DeviceRepair.Core.Dtos.Output.RepairOrder;
namespace DeviceRepair.Core.Interfaces
{
public interface IOrderDapperRepository
{
Task<PageResult<OrderPageOutput>> GetOrderPageListAsync(PageQuery dto);
}
}
OrderDapperRepository.cs
using DeviceRepair.Core.Dtos.Common;
using DeviceRepair.Core.Dtos.Output.RepairOrder;
using DeviceRepair.Core.Interfaces;
using DeviceRepair.Infrastructure.Utils;
namespace DeviceRepair.Infrastructure.Repositories
{
public class OrderDapperRepository : IOrderDapperRepository
{
public async Task<PageResult<OrderPageOutput>> GetOrderPageListAsync(PageQuery dto)
{
var pageResult = new PageResult<OrderPageOutput>
{
PageIndex = dto.PageIndex,
PageSize = dto.PageSize
};
string countSql = @"SELECT COUNT(1) FROM RepairOrders o LEFT JOIN Users u ON o.UserId=u.Id LEFT JOIN Devices d ON o.DeviceId=d.Id WHERE o.IsDeleted=0";
pageResult.Total = await DapperHelper.ExecuteAsync(countSql);
int skip = (dto.PageIndex - 1) * dto.PageSize;
string dataSql = $@"
SELECT o.Id, u.UserName, d.DeviceName, o.ProblemDesc, o.OrderStatus, o.CreateTime
FROM RepairOrders o
LEFT JOIN Users u ON o.UserId = u.Id
LEFT JOIN Devices d ON o.DeviceId = d.Id
WHERE o.IsDeleted = 0
ORDER BY o.CreateTime DESC
OFFSET {skip} ROWS FETCH NEXT {dto.PageSize} ROWS ONLY";
pageResult.Data = await DapperHelper.QueryAsync<OrderPageOutput>(dataSql);
return pageResult;
}
}
}
4. SystemMessageController 改造(自行参照上面规则完成)
规则一致:入参用 MessageInput、出参用 MessageOutput,调用 ModelMapper 映射。
七、测试验证
- 清理并重新生成解决方案,确认无编译报错
- 启动项目,打开 Swagger
- 全流程测试:
- 登录接口:接收
LoginInput,正常返回 Token - 设备 / 工单 / 消息 增删改查:入参校验生效、返回精简 DTO、无敏感字段
- 分页接口:传入
PageIndex/PageSize,正常返回分页结构体
- 登录接口:接收
- 检查数据库实体不再直接暴露给前端,安全性达标
本阶段总结
✅ DTO 分层规范(Common/Input/Output) ✅ 通用分页模型全局复用 ✅ 实体与 DTO 手动映射工具类统一管理 ✅ 所有接口完成 DTO 改造,隔离数据库实体 ✅ 参数校验、字段长度约束统一配置
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)