让我重新审视我的架构的截止日期:使用 Domain Thinking 构建 Timesheetflow

发布: (2026年1月4日 GMT+8 21:58)
8 min read
原文: Dev.to

Source: Dev.to

那一个问题改变了我对最初认为的 “仅仅是 Excel 处理”。
我在构建 Timesheetflow——一个小型系统,用于自动化每月仍以 Excel 文件提交的员工工时表(因为实际上,许多团队仍然使用电子表格)。

实际工作流程(业务真实操作)

Timesheetflow 的工作流程简单——且非常真实:

  1. 管理员设置处理截止日期(通常是月末)。
  2. 员工在截止日期前将自己的月度工时表上传到 Google Drive 文件夹 (仅提交当月的工时表)
  3. 截止日期,系统 逐个处理工时表。
  4. 审批人审查并批准一个、多个或全部工时表。
    • 迟交的仍可接受,但会明确标记为 迟交
  5. 管理员下载该月的 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 更重要)

  1. 仅限当前月份 – 员工只能提交当月的工时表。
  2. 截止时间锁定运行 – 在截止时间,运行进入“锁定”阶段以进行处理。
  3. 允许迟交,但会标记 – 截止后提交的工时表会标记为 Late,并在审批/导出中保持可见。
  4. 最新提交获胜 – 如果员工为同一月份上传多个工时表:
    • 系统使用最近的文件(依据 Drive 时间戳)
    • 较早的文件会标记为 superseded
  5. 无效优先于迟交 – 如果工时表无效,无论是否迟交,都视为无效。
    • Late 只是一个标记。
    • Invalid 是阻断因素。
  6. 工资导出是管理员交付物 – 最终结果不是“已处理的行”;而是可供薪资/管理员使用的月度 Excel 汇总。

实现说明(技术栈)

  • ASP.NET Core (.NET 8) – Web + API
  • Hangfire – 截止日期调度和后台处理
  • ClosedXML – Excel 解析/导出
  • Google Drive – 提交渠道(Workspace 文件夹 + 服务账户访问)

即使使用了简单的技术栈,清晰的领域建模也让系统更像“产品”,而不是“脚本”。

Key Takeaways

  • 这实际上并不是 Excel 的问题——而是一个 月末工资结算 的问题。
  • 截止日期迟交标记“最晚提交获胜”领域规则,不仅仅是工具逻辑。
  • 围绕这些规则构建通用语言,使代码读起来像业务流程,从而更容易维护并进行未来的更改。

当代码映射业务语言时

  • 当代码映射业务语言时,它变得更易于阅读、测试和扩展。
  • 最佳架构不仅仅是运行——它传达意图。

结束语

我最初创建 Timesheetflow 时,以为自己在构建一个 Excel 自动化工具。
真正的价值在于,我不再围绕文件进行设计,而是围绕业务每月所遵循的工作流进行设计。

如果你曾经遇到过一个“简单自动化”实际上是伪装的领域问题的时刻,我很想听听你的经历。

Source:

与我联系

  • GitHub:
  • LinkedIn:

如果你愿意,我还可以分享:

  • 一个用于 .NET 8 的最小领域模型(MonthlyRunTimesheetSubmissionApproval
  • 一个用于截止日期 + 处理流水线的 Hangfire 作业布局
  • 一个示例 Salary_Summary.xlsx 结构(ByEmployee / Details / Exceptions

只需告诉我你希望文章包含 可编译的代码(完整示例类)还是保持 概念性代码片段(如本帖所示)。

Back to Blog

相关文章

阅读更多 »

Go 中优雅的领域驱动设计对象

❓ 你如何在 Go 中定义你的领域对象?Go 并不是典型的面向对象语言。当你尝试实现 Domain‑Driven Design(DDD)概念,如 Entity …

让我们分离分离

简介 在2025年最后几天,我们的团队负责人额外请了一天假,错过了一个重要会议。最近的重组后,一位同事离职……