跳到主要内容

轻量 DDD:ToDo 切片

ToDo 是 Platform 模块中的一个完整垂直切片,适合观察 FreeKitModules 的实际分层。它没有为了形式增加独立 Manager,而是由实体承载局部行为、Application Service 编排用例、审计仓储负责持久化、HttpApi Controller 暴露接口。

目录边界

src/Services/Platform/
├── FreeKit.Platform/
│ └── ToDo/
│ ├── Application/
│ │ ├── ToDoService.cs
│ │ ├── INotificationScheduler.cs
│ │ └── FreeSchedulerNotificationAdapter.cs
│ ├── Contracts/
│ │ ├── IToDoService.cs
│ │ ├── InputToDo.cs
│ │ ├── ToDoDto.cs
│ │ └── ToDoQuery.cs
│ ├── Data/
│ │ └── ToDoDbContext.cs
│ └── Domain/Entities/
│ ├── ToDo.cs
│ └── ToDoConfiguration.cs
└── FreeKit.Platform.HttpApi/
└── ToDo/Controllers/ToDoController.cs

这说明 DDD 的目标是保护业务边界,不是要求每个功能都必须具备相同数量的层和类型。只有出现跨实体不变量或可复用领域规则时,才需要增加 IToDoManager / ToDoManager

实体

FreeKit.ToDos.Domain.Entities.ToDo 继承 FullAuditEntity,使用 Guid 主键并包含完整审计/软删除字段。自己的字段为:

字段类型含义
Messagestring待办内容,表映射限制为 500 字符
NotificationTimeDateTime?提醒时间
IsNotificationbool?是否已经投递提醒
IsDonebool是否完成
Sortint升序排序值
DoneTimeDateTime?完成时间

实体封装了三个当前行为:

public void MakeNotification()
{
IsNotification = true;
}

public void MarkDone(bool isDone)
{
IsDone = isDone;
DoneTime = isDone ? DateTime.Now : null;
}

public bool CanBeAccessedBy(Guid userId)
{
return CreateUserId == userId;
}

MarkDone 中的 DateTime.Now 是存量实现。仓库的新代码约定使用 DateTime.UtcNow;修复存量时间语义时需要同时考虑已有数据和 API 序列化,不能只做全局替换。

Application Service

ToDoService 的依赖反映了用例边界:

public class ToDoService(
IAuditBaseRepository<ToDo> todoRepository,
ICurrentUser currentUser,
INotificationScheduler notificationScheduler)
: ApplicationService, IToDoService;

职责分配如下:

  • IAuditBaseRepository<ToDo>:查询、插入、更新和软删除。
  • ICurrentUser:限定数据属于当前创建者。
  • INotificationScheduler:隔离 FreeScheduler,不让业务服务直接依赖具体调度 API。
  • ApplicationService:提供日志、权限、工作单元等共享能力。

列表查询始终附带:

.Where(x => x.CreateUserId == currentUser.FindUserId())

详情、更新、删除、完成和排序操作都会调用 CanBeAccessedBy 检查归属。批量完成和排序只要发现一条非当前用户数据,就返回错误而不继续更新。

HTTP API

基础路由是 /api/plat/todo,Swagger 分组为 plat。开发环境主 Host 还会叠加 /kit_api PathBase。

方法路由认证用途
GET/api/plat/todo登录当前用户分页列表,可按 MessageIsDone 过滤
GET/api/plat/todo/{id:guid}源码标记匿名详情;Service 仍要求可解析当前用户并校验归属
POST/api/plat/todo登录创建待办
PUT/api/plat/todo/{id}登录更新待办,并可能创建提醒任务
DELETE/api/plat/todo/{id}登录软删除
PUT/api/plat/todo/done/{id}/{isDone}登录完成或取消完成
PUT/api/plat/todo/dones登录批量完成或取消完成
PUT/api/plat/todo/sort登录按请求中的 Id 顺序保存 Sort

详情 Action 当前带 [AllowAnonymous],但 ToDoService.GetAsync 仍调用 currentUser.FindUserId() 并执行归属检查。这是不一致的存量边界,不应作为新接口模板;若要真正匿名读取,需要重新定义资源可见性,若不需要则应移除匿名标记。

Controller 保持薄转发:

[HttpPut("{id}")]
public Task<IResult> UpdateAsync(
Guid id,
[FromBody] InputToDo input)
{
return toDoService.UpdateAsync(id, input);
}

提醒链路

当前提醒不是在创建时调度,而是在更新时处理:

精确语义:

  • CreateAsync 当前只插入实体,不调度提醒。
  • UpdateAsync 只在 NotificationTime > DateTime.Now 时调用调度器。
  • Adapter 将延迟换算为秒;若被其他调用方传入过去时间,会把延迟压到 0,立即触发。
  • FreeScheduler topic 为 todo-notification-{todoId},Body 为 todoId
  • Job Handler 跳过不存在、已完成、已提醒、无提醒时间或无创建者的记录。
  • 最终投递使用 CmsKit 的 INotificationService 创建系统通知,不经过 Platform IPlatformMessageService
  • Job 在投递前设置 IsNotification,降低任务重放造成重复通知的风险。

实现分别位于:

  • FreeSchedulerNotificationAdapter.cs
  • src/Services/Host/FreeKit.Job.Host/Extensions/JobExtensions.cs

持久化

ToDoConfiguration 将实体映射到 todo_item,主键为 Id,并把 Message 长度限制为 500。ToDoDbContext 从现有 IFreeSql 构造,禁止在 DbContext 中再次 Build() FreeSql 实例;结构同步只在 DEBUG 编译下执行。

应用层通过 IAuditBaseRepository<ToDo> 写入,删除操作把 IsDeleted 置为 true 后更新。新功能不要绕过仓储直接注入 IFreeSql

当前技术债与新增规则

当前存量新代码规则
CreateAsyncMarkDone 使用 DateTime.Now数据库存储时间使用 DateTime.UtcNow
IToDoService.MakeDone / MakeDones 返回 Task 但无 Async 后缀Application/Domain/Infrastructure 异步业务方法以 Async 结尾
详情 Action 标记匿名但仍要求当前用户认证特性与 Service 可见性规则必须一致
只有 Update 会调度提醒若产品要求创建即提醒,应在同一事务语义下显式补充并测试

源码索引

  • src/Services/Platform/FreeKit.Platform/ToDo/Domain/Entities/ToDo.cs
  • src/Services/Platform/FreeKit.Platform/ToDo/Application/ToDoService.cs
  • src/Services/Platform/FreeKit.Platform/ToDo/Application/FreeSchedulerNotificationAdapter.cs
  • src/Services/Platform/FreeKit.Platform.HttpApi/ToDo/Controllers/ToDoController.cs
  • src/Services/Host/FreeKit.Job.Host/Extensions/JobExtensions.cs

相关文档