轻量 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 主键并包含完整审计/软删除字段。自己的字段为:
| 字段 | 类型 | 含义 |
|---|---|---|
Message | string | 待办内容,表映射限制为 500 字符 |
NotificationTime | DateTime? | 提醒时间 |
IsNotification | bool? | 是否已经投递提醒 |
IsDone | bool | 是否完成 |
Sort | int | 升序排序值 |
DoneTime | DateTime? | 完成时间 |
实体封装了三个当前行为:
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 | 登录 | 当前用户分页列表,可按 Message、IsDone 过滤 |
| 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创建系统通知,不经过 PlatformIPlatformMessageService。 - Job 在投递前设置
IsNotification,降低任务重放造成重复通知的风险。
实现分别位于:
FreeSchedulerNotificationAdapter.cssrc/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。
当前技术债与新增规则
| 当前存量 | 新代码规则 |
|---|---|
CreateAsync、MarkDone 使用 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.cssrc/Services/Platform/FreeKit.Platform/ToDo/Application/ToDoService.cssrc/Services/Platform/FreeKit.Platform/ToDo/Application/FreeSchedulerNotificationAdapter.cssrc/Services/Platform/FreeKit.Platform.HttpApi/ToDo/Controllers/ToDoController.cssrc/Services/Host/FreeKit.Job.Host/Extensions/JobExtensions.cs