让我重新审视我的架构的截止日期:使用 Domain Thinking 构建 Timesheetflow
Source: Dev.to
那一个问题改变了我对最初认为的 “仅仅是 Excel 处理”。
我在构建 Timesheetflow——一个小型系统,用于自动化每月仍以 Excel 文件提交的员工工时表(因为实际上,许多团队仍然使用电子表格)。
实际工作流程(业务真实操作)
Timesheetflow 的工作流程简单——且非常真实:
- 管理员设置处理截止日期(通常是月末)。
- 员工在截止日期前将自己的月度工时表上传到 Google Drive 文件夹 (仅提交当月的工时表)。
- 在 截止日期,系统 逐个处理工时表。
- 审批人审查并批准一个、多个或全部工时表。
- 迟交的仍可接受,但会明确标记为 迟交。
- 管理员下载该月的 Excel 薪资汇总。
当我把它写下来时,我意识到:
这不是一个“Excel 自动化”问题。
这是一项 月度工资结算 问题。
我原以为的问题
我的第一种方法非常偏向实现细节:
- “有一个 Drive 文件夹。”
- “里面有 Excel 文件。”
- “我需要解析并导出。”
于是我的架构自然变成了一个 流水线:
- Drive 客户端下载文件
- 解析器提取行数据
- 验证器检查列
- 导出器生成合并报告
看起来很整洁……但代码一直在读:
“更新表格,移动文件,生成输出”
而不是:
“关闭月份,执行截止日期,批准提交,完成工资单”
这种差别听起来微妙,却会改变一切——尤其是在需求演变时。
我事后意识到的
好的架构不仅仅是层次和模式。
它关乎 意义。
当我把月末工作流视为一个 领域(而不是批处理任务)时,设计变得更清晰:
- 截止日期不是一个时间戳字段——它是一条改变系统行为的规则。
- “迟交”不是一个警告标签——它影响审批的可见性和可审计性。
- “最新提交获胜”不是一个查询技巧——它是业务依赖的策略。
我开始使用的通用语言
一旦我不再用“文件”思考,而是用“业务”思考,词汇就变得显而易见:
| 术语 | 含义 |
|---|---|
| Monthly Run / Payroll Period | 正在结算的月份 |
| Deadline | 截止时间 |
| Timesheet Submission | 员工的月度记录提交 |
| Late Submission | 允许,但会被标记 |
| Latest Submission Wins | 员工上传多个版本时的策略 |
| Approval | 批准/拒绝;批量批准有效 |
| Salary Summary Export | 管理员交付物 |
当你的代码使用这种语言时,就不需要冗长的注释来解释它的作用。
Source:
前后对比(管道思维 vs. 领域思维)
之前:“处理文件”
// Pipeline mindset: process whatever is in the folder
var files = drive.ListXlsxFiles(folderId);
foreach (var file in files)
{
var rows = ParseExcel(file);
Validate(rows);
Save(rows);
}
ExportSalarySummary();
它能工作——但隐藏了真实规则:
- 这是哪一个月?
- 截止日期是否得到强制执行?
- 如果员工上传两次,哪个文件会被采用?
- 审批后会发生什么?
之后:“关闭一次月度运行”
// Domain mindset: close a payroll period with explicit rules
run.LockAtDeadline(now);
run.RefreshSubmissionsFromDrive(); // latest submission wins
run.FlagLateSubmissions(); // uploaded after deadline
foreach (var submission in run.ActiveSubmissions())
{
submission.ValidateCurrentMonthOnly();
}
approvals.ApproveAllValid(run); // late is a badge, invalid blocks approval
var report = payroll.ExportSalarySummary(run); // Excel for admin
现在代码像业务流程一样易读:
- 锁定期间
- 在已知规则下评估提交
- 批准有效的内容
- 导出工资汇总
这种转变让维护和解释变得更容易。
关键领域规则(比 Excel 更重要)
- 仅限当前月份 – 员工只能提交当月的工时表。
- 截止时间锁定运行 – 在截止时间,运行进入“锁定”阶段以进行处理。
- 允许迟交,但会标记 – 截止后提交的工时表会标记为 Late,并在审批/导出中保持可见。
- 最新提交获胜 – 如果员工为同一月份上传多个工时表:
- 系统使用最近的文件(依据 Drive 时间戳)
- 较早的文件会标记为 superseded
- 无效优先于迟交 – 如果工时表无效,无论是否迟交,都视为无效。
- Late 只是一个标记。
- Invalid 是阻断因素。
- 工资导出是管理员交付物 – 最终结果不是“已处理的行”;而是可供薪资/管理员使用的月度 Excel 汇总。
实现说明(技术栈)
- ASP.NET Core (.NET 8) – Web + API
- Hangfire – 截止日期调度和后台处理
- ClosedXML – Excel 解析/导出
- Google Drive – 提交渠道(Workspace 文件夹 + 服务账户访问)
即使使用了简单的技术栈,清晰的领域建模也让系统更像“产品”,而不是“脚本”。
Key Takeaways
- 这实际上并不是 Excel 的问题——而是一个 月末工资结算 的问题。
- 截止日期、迟交标记 和 “最晚提交获胜” 是 领域规则,不仅仅是工具逻辑。
- 围绕这些规则构建通用语言,使代码读起来像业务流程,从而更容易维护并进行未来的更改。
当代码映射业务语言时
- 当代码映射业务语言时,它变得更易于阅读、测试和扩展。
- 最佳架构不仅仅是运行——它传达意图。
结束语
我最初创建 Timesheetflow 时,以为自己在构建一个 Excel 自动化工具。
真正的价值在于,我不再围绕文件进行设计,而是围绕业务每月所遵循的工作流进行设计。
如果你曾经遇到过一个“简单自动化”实际上是伪装的领域问题的时刻,我很想听听你的经历。
Source:
与我联系
- GitHub:
- LinkedIn:
如果你愿意,我还可以分享:
- 一个用于 .NET 8 的最小领域模型(
MonthlyRun、TimesheetSubmission、Approval) - 一个用于截止日期 + 处理流水线的 Hangfire 作业布局
- 一个示例
Salary_Summary.xlsx结构(ByEmployee / Details / Exceptions)
只需告诉我你希望文章包含 可编译的代码(完整示例类)还是保持 概念性代码片段(如本帖所示)。