后端前端搜索引擎网页爬虫数据工程AI 应用可观测性【免费下载链接】freehirefreehire — the open-source search engine for job seekers项目地址https://gitcode.com/GitHub_Trending/fr/freehire点击查看免费下载本篇以归档变更openspec/changes/archive/2026-06-14-extract-job-tracking-service的任务清单为主体详解 freehire 如何将用户-岗位交互view/apply/save/unsave/track这一组用例从user_jobs.gohandler 中剥离为独立的jobtracking服务层。读完后你能掌握一套可复制的重构路径先固化现有行为characterization test再引入窄Repository接口与Service最后把 handler 瘦身成纯传输层并知道如何保证 HTTP 线格式wire format零漂移。1. 背景HTTP 层混杂了三种职责变更提案 proposal.md 指出的核心问题HTTP 层同时承担了三类职责——handler 直接持有pgxpool.Pool和具体的*db.Queries每用户的岗位跟踪规则stage 校验、nil 字段保持不变的部分更新语义、slug→id 解析、pgtype↔ JSON 转换内联在user_jobs.go中这些规则只能通过完整的 Fiber 请求来驱动无法被单元测试也无法在非 HTTP 场景复用。这是整个服务层抽取计划中第一个、风险最低的切片先在一个低风险面上确立service repo interface thin handler的模式之后再处理风险更高的 auth/OAuth 代码。设计文档 design.md 明确了 Goals业务规则可在无 Fiber、无数据库的条件下用 fakeRepository单测HTTP 行为逐字节不变与 Non-Goals不改 SQL/schema、不加新查询、不动路由和中间件。2. 任务清单全景五个阶段的 TDD 重构核心执行计划记录在 tasks.md所有任务均已勾选完成脉络分为五个阶段阶段 1Pin current behavior固化现有行为1.1确认既有的user_jobs集成测试//go:build integration真实 Postgres覆盖了成功路径的 wire shapejob_id RFC3339/null 时间戳、save/unsave 契约、404。它是端到端的 no-drift 守卫合并前必须通过go test -tagsintegration ./internal/handler/需要 Docker且不得重写它。1.2映射函数提取后任务 4.x补一个无数据库的单测把Interaction → interactionResponse的 JSON 编码钉死精确字段名、RFC3339 时间戳字符串、viewed/saved/applied/stage/notes 缺席时的null。阶段 2jobtracking 包——契约与规则TDD fake repo2.1建internal/jobtracking包定义领域Interaction结构体JobID int64ViewedAt/SavedAt/AppliedAt *time.TimeStage/Notes *string与哨兵错误ErrJobNotFound、ErrInvalidStage、ErrEmptyTrack2.2在包内定义窄Repository接口JobIDBySlug、RecordView、MarkApplied、SaveJob、UnsaveJob、TrackJob2.3用 fakeRepository写失败单测record view / mark applied / save 各自返回映射后的Interaction2.4实现Service.RecordView/MarkApplied/SaveJob经 repo 做 slug→id 解析未知 slug 返回ErrJobNotFound使 2.3 通过2.5写失败单测验证Unsave幂等无行 → 零值Interaction{JobID}nil error并实现2.6写失败单测验证Track非法 stage →ErrInvalidStagestage 和 notes 都没有 →ErrEmptyTrack仅 stage / 仅 notes / 两者都给时另一个字段保持不变。用userjob.ValidStage实现校验。阶段 3持久化适配器3.1在internal/jobtracking内加一个包装*db.Queries的Repository适配器完成db.UserJobpgtype→Interaction的转换以及db错误 → 包内哨兵错误的映射pgx.ErrNoRows→ slug 查找时的ErrJobNotFoundunsave 的 no-row 情形在 service 中处理3.2确认包可构建且go test ./internal/jobtracking/全绿。阶段 4Handler 瘦身4.1在handler.Register中构造 servicejobtracking.New(jobtracking.NewQueriesRepository(queries))并挂到Handler上4.2重写RecordView/MarkApplied/SaveJob/UnsaveJob/TrackJob读auth.UserID、解析 body仅 track 需要、以(userID, slug, …)调 service、把领域错误映射为 HTTP 状态码ErrJobNotFound→404ErrInvalidStage/ErrEmptyTrack→400、把Interaction渲染为既有的interactionResponseJSON4.3删除user_jobs.go里已死的内联规则interactionParams/trackRequest等被 service 取代的管道代码这些端点不再调用h.queries。阶段 5验证5.1go test -tagsintegration ./internal/handler/真实 Postgres确认 1.1 的 user_jobs wire 契约保持绿色。若开发环境没有 Docker需记录该套件必须先在 CI 通过再合并映射单测1.2则是开发循环内的守卫5.2go build ./...、go vet ./...、go test ./...全部通过5.3自审确认internal/jobtracking中没有pgtype/pgx/fiber导入且user_jobs.go中不再残留任何业务规则stage 校验、部分更新、幂等性、slug 解析。3. 领域契约Interaction 与哨兵错误任务 2.1 定义的领域模型在当前 jobtracking.go 中落地Interaction定义于第 19–28 行// Interaction is the storage-agnostic result of a per-user job interaction. type Interaction struct { JobID int64 ViewedAt *time.Time SavedAt *time.Time AppliedAt *time.Time // DismissedAt 为后续变更加入 DismissedAt *time.Time Stage *string Notes *string }设计决策 2见 design.md解释了为什么返回领域Interaction而不是db.UserJobpgtype是持久化细节不允许泄漏进与传输无关的 service 契约被否决的替代方案是返回db.UserJob——那会让 service 和 handler 双双重新耦合到 pgtype。哨兵错误jobtracking.go第 145–160 行是 service 契约的一部分。对照 tasks.md 5.3 的要求service 返回的语义化错误让 handler 的 404/400 决策留在 tracking 领域内而不是把pgx.ErrNoRows透传给中央ErrorHandler哨兵错误语义handler 映射ErrJobNotFound未知 slug404ErrInvalidStagestage 不在受控词表中400ErrEmptyTrackstage/notes 均未提供400ErrNoInteraction无可清除的交互行不外泄由 Service 转为零值成功—stage 词表由 stages.go 中的userjob.Stages/ValidStage提供是单一事实来源preparing/applied/screening/responded/interview/offer/accepted/rejected/withdrawn/expiredtrack 端点据此校验前端列表也从它生成。4. 窄 Repository 接口与持久化适配器Repository接口jobtracking.go 第 162 行起遵循设计决策 1由消费者定义接口。任务清单 2.2 中的最初版本只有 6 个方法当前接口已随后续切片扩展dismiss、filter 列表、pipeline、application-id 寻址的写操作等但接口最小化、方法必须被 service 实际调用的原则保持不变。适配器 repository.go 完成了任务 3.1 描述的全部映射关键实现点编译期证明第 22 行var _ Repository (*QueriesRepository)(nil)slug 解析第 38–47 行pgx.ErrNoRows→ErrJobNotFoundpgtype 转换textPtr把pgtype.Text转为*stringNULL → niltextFromPtr反向转换nil 产生 invalidNULLText使 SQL 的COALESCE保留列原值——这就是 track 端点nil 字段保持不变部分更新语义的存储层落点事务化 applymarkApplied第 133–155 行先LockJobForApply锁岗位行再写使并发 apply 串行化、applied_count不会重复自增且applied账本事件与计数器在同一事务、同一谓词下落库二者不可能分叉unsave 的 no-row 语义第 167–176 行pgx.ErrNoRows→ErrNoInteraction由 Service 侧转成零值成功任务 2.5 的幂等性规则。5. 业务规则在 Service 中的位置对照任务 2.3–2.6 的规则要求jobtracking.go 中的关键行为slug→id 解析RecordView/SaveJob/Unsave等每个写用例都先经s.repo.JobIDBySlug解析未知 slug 直接返回ErrJobNotFound不触及业务写Track 的预校验第 558–570 行stage nil notes nil→ErrEmptyTrackstage ! nil !userjob.ValidStage(*stage)→ErrInvalidStage。校验发生在 slug 查找之前——坏请求永远不碰数据库Unsave 幂等第 490–505 行repo 返回ErrNoInteraction时service 将其替换为Interaction{JobID: jobID}, nil——已经没保存是成功而非错误部分更新stage/notes 以*string指针传入 reponil 即不动。单测 jobtracking_test.go 用内存 fakefakeRepo第 15–191 行无 Fiber、无数据库逐一钉死了上述规则且测试断言了短路行为例如TestTrack_NilNil_ReturnsErrEmptyTrack在断言错误的同时检查repo.slugCalls 0确保校验确实在数据库之前短路。TestUnsave_NoInteraction_ReturnsZero则验证零值交互除JobID外全为 nil。6. 薄 Handler错误映射与线格式重构后的传输层在 user_jobs.go变更文档写作时路径为internal/handler/user_jobs.go后随仓库结构调整迁移到internal/api/handler/。五个 tracking handler 现在做且仅做四件事取auth.UserID、解析 body、调 service、渲染{data: …}。wire shape第 89–100 行interactionResponse省略user_id时间戳字段用*time.Time、stage/notes 用*string未发生交互时序列化为null错误映射trackingError第 119–138 行ErrJobNotFound→404 job not foundErrInvalidStage/ErrEmptyTrack→400其余错误透传给中央RenderError落 500组合根handler.go 第 798 行tracking : jobtracking.New(jobtracking.NewQueriesRepository(queries, cfg.Pool), jobtracking.WithReminders(reminders))——service 只构造一次。7. 双层守卫集成测试 映射单测设计决策 3 解释了 wire 漂移风险的处置把pgtype.Timestamptz/pgtype.Text换成*time.Time/*string可能引入微妙的 JSON 格式漂移而 handler 单测跑在Handler{} nil DB 上只能覆盖到打数据库之前的拒绝路径无法刻画成功路径的 JSON。因此用两层守卫覆盖端到端user_jobs_integration_test.go//go:build integration真实 Postgres把成功响应解码进{job_id, viewed_at, saved_at, applied_at}的*string时间戳结构体并断言 save/unsave 契约与 404——这正是 tasks.md 1.1 要不得重写的 no-drift 守卫。运行命令为go test -tagsintegration ./internal/api/handler/需 Docker。其三个子测试分别是无交互行时 unsave 返回 200 零状态且user_jobs表不产生新行、save→unsave 往返unsave 后saved_at为 null 但viewed_at保留、未知 slug 返回 404单元测试钉住提取出的Interaction → interactionResponse映射——字段名、RFC3339 字符串、缺席字段的null无数据库、跑在快速套件里。这个拆分的价值单测在开发循环内快速捕捉格式/字段漂移集成测试是合并前CI 或本地有 Docker 时的权威端到端检查。8. 验证清单与后续演化tasks.md 第 5 节的验证标准值得作为任何提取服务层类重构的收尾清单go test -tagsintegration相关包全绿wire 契约无漂移go build ./...、go vet ./...、go test ./...全绿依赖自审service 包内不得出现pgtype/pgx/fiber导入handler 中不得残留 stage 校验、部分更新、幂等、slug 解析等业务规则。从源码结构看这次提取确立的模式已被后续变更持续复用jobtracking包在 5 个初始用例之外扩展了 dismiss/undismiss、filter 列表与分页ListTracked、pipeline 统计、reminder 副作用端口WithReminders让 in-app assistant 等非 HTTP 调用方也能触发同样的提醒语义以及面向岗位被删除后孤儿申请的 application-id 寻址写操作TrackApplication/UntrackApplication/ClearApplicationProgress——jobtracking_test.go 中TestSaveJob_SchedulesTheReminder等用例注释明确写道这正是把规则从 HTTP 门口移到 service的收益经由 assistant 直接调 service 的保存从此也能正确产生提醒行。换言之extract-job-tracking-service这份任务清单不只是一次孤立的重构而是 freehire 服务层模式窄接口 领域哨兵错误 双层测试守卫的起点。赞分享后端前端搜索引擎网页爬虫数据工程AI 应用可观测性【免费下载链接】freehirefreehire — the open-source search engine for job seekers项目地址https://gitcode.com/GitHub_Trending/fr/freehire点击查看免费下载相关推荐freehire 认证服务抽离实战把 OAuth 账号解析策略从 HTTP Handler 中剥离为可单测的 Service Repositoryfreehire 认证服务抽离实战把 OAuth 账号解析策略从 HTTP Handler 中剥离为可单测的 Service Repository 本篇技后端前端搜索引擎网页爬虫数据工程AI 应用可观测性freehire OAuth 账户解析重构把最危险的认证策略从 Handler 抽成可单元测试的 Service Repositoryfreehire OAuth 账户解析重构把最危险的认证策略从 Handler 抽成可单元测试的 Service Repository 这篇技术文章基于后端前端搜索引擎网页爬虫数据工程AI 应用可观测性freehire 职位跟踪服务层抽离用「用例 Service 窄 Repository 接口 薄 Handler」重建 Handler 分层freehire 职位跟踪服务层抽离用「用例 Service 窄 Repository 接口 薄 Handler」重建 Handler 分层 本文基于后端前端搜索引擎网页爬虫数据工程AI 应用可观测性上一篇如何快速构建超强安全架构浏览器Ladybird图像解码与网络连接完全隔离的终极指南下一篇DFlash架构深度解析从滑动窗口注意力到因果SWA层的技术演进创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?