全栈项目从 0 到 1 实战(5):核心业务接口开发

发布时间:2026/8/28 20:18:03
全栈项目从 0 到 1 实战(5):核心业务接口开发 上一篇建立了身份、会话和资源级授权本篇把它们接入任务业务接口。我们不满足于“CRUD 能跑”而要做出稳定契约输入有边界、列表可分页、重复请求不重复创建、并发更新不静默覆盖所有失败都能被前端和日志准确识别。一、痛点HTTP 接口不是数据库表的遥控器若路由直接暴露 ORM 模型客户端会依赖内部列名攻击者还可能提交workspace_id、created_by等不可写字段。接口应使用独立输入输出模式只允许白名单字段。创建任务的工作区来自已验证上下文创建者来自当前身份项目归属由服务端查询永远不信任客户端重复传来的安全边界。本篇约定POST /api/v1/projects/{project_id}/tasks创建GET列表GET /tasks/{id}详情PATCH部分更新。成功创建返回 201 和Location删除若采用幂等语义返回 204校验失败返回 422资源不可见返回 404版本冲突返回 409。错误体固定包含code、message、details、request_id前端根据稳定的 code 分支不解析自然语言。二、原理业务规则集中在服务层路由层负责 HTTP 解析与身份依赖服务层负责事务和业务不变量仓储层封装带租户条件的查询。这样 CLI、后台任务和 API 可复用同一规则。下面的独立程序实现创建命令校验明确区分字段缺失、长度、状态和值域错误。fromdataclassesimportdataclassfromdatetimeimportdate ALLOWED_PRIORITIES{low,medium,high}dataclass(frozenTrue)classCreateTask:title:strpriority:strdue_date:date|Nonedefvalidate(payload:dict)-CreateTask:errors[]titlepayload.get(title,).strip()ifnottitle:errors.append({field:title,code:required})eliflen(title)120:errors.append({field:title,code:too_long})prioritypayload.get(priority,medium)ifprioritynotinALLOWED_PRIORITIES:errors.append({field:priority,code:invalid_choice})due_dateNoneifpayload.get(due_date):try:due_datedate.fromisoformat(payload[due_date])except(TypeError,ValueError):errors.append({field:due_date,code:invalid_date})iferrors:raiseValueError(errors)returnCreateTask(title,priority,due_date)commandvalidate({title: 发布 API ,priority:high,due_date:2026-08-20})print(ftitle{command.title})print(fpriority{command.priority}due{command.due_date.isoformat()})try:validate({title: ,priority:urgent})exceptValueErroraserror:print(finvalid_fields{len(error.args[0])})运行输出title发布 API priorityhigh due2026-08-20 invalid_fields2校验分三层语法层检查类型和格式业务层检查项目是否归属工作区、负责人是否是成员数据库层用约束兜底。重复校验并非浪费每层提供不同失败体验和安全边界。服务层将数据库唯一冲突翻译成领域错误而非把 SQL 文本泄漏给客户端。三、实现幂等与并发都要显式设计网络超时时客户端不知道创建是否成功直接重试可能产生两个任务。创建请求接受Idempotency-Key服务端以“用户、端点、键”做唯一索引保存请求体摘要和最终响应同键同请求返回原响应同键不同请求返回 409。记录与业务写入在同一事务键设置 24 小时过期并有清理任务。更新使用乐观并发。任务带整数version客户端读取后在If-Match或请求体提交期望版本SQL 使用UPDATE ... SET versionversion1 WHERE id? AND workspace_id? AND version?。影响零行时区分不存在与冲突。下面内存实现展示幂等创建和版本保护的完整行为。importhashlibimportjson tasks{}idempotency{}next_id1deffingerprint(payload:dict)-str:rawjson.dumps(payload,sort_keysTrue,separators(,,:))returnhashlib.sha256(raw.encode()).hexdigest()defcreate_task(user_id:str,key:str,payload:dict)-tuple[dict,bool]:globalnext_id scope(user_id,create_task,key)digestfingerprint(payload)ifscopeinidempotency:saved_digest,task_ididempotency[scope]ifsaved_digest!digest:raiseValueError(idempotency_key_reused)returntasks[task_id].copy(),Truetask{id:next_id,title:payload[title],version:1}tasks[next_id]task idempotency[scope](digest,next_id)next_id1returntask.copy(),Falsedefrename_task(task_id:int,expected_version:int,title:str)-dict:tasktasks.get(task_id)iftaskisNone:raiseKeyError(not_found)iftask[version]!expected_version:raiseRuntimeError(version_conflict)task[title]title task[version]1returntask.copy()first,replayed1create_task(u1,key-123,{title:联调})second,replayed2create_task(u1,key-123,{title:联调})updatedrename_task(first[id],1,完成联调)print(fsame_id{first[id]second[id]}replay{replayed1},{replayed2})print(ftitle{updated[title]}version{updated[version]})运行输出same_idTrue replayFalse,True title完成联调 version2列表查询只开放受控排序字段防止把用户输入拼进 SQL。默认限制 20最大 100返回items与next_cursor不强制每次计算昂贵总数。过滤器写入 OpenAPI空列表返回 200 和空数组。PATCH 需要区分字段未提供与显式设为 null模式库的 unset 语义要测试清楚。每次写操作同时写审计事件和 outbox。审计记录“谁、何时、在哪个工作区、对什么资源、做何动作、结果如何”敏感正文按需要摘要化。outbox worker 至少一次投递因此消费者按事件 ID 幂等事件模式带版本新增字段保持向后兼容。四、踩坑自动重试会放大非幂等副作用数据库死锁或网络断开可以重试但重试边界必须包住完整事务且事务内不能直接发邮件。客户端对 GET 可安全退避重试POST 只有提供幂等键才自动重试。错误不能全部变成 500预期的业务拒绝应是明确 4xx同时也不要把内部异常、SQL、堆栈返回浏览器。接口版本不是每次改动都加/v2。新增可选字段通常兼容删除字段、改变类型或语义才需要迁移计划。先通过弃用响应头和文档通知观测旧字段使用量给客户端迁移窗口再移除。OpenAPI 是契约但仍需契约测试阻止意外破坏。五、验证从正常路径扩展到竞争条件为每个端点测试成功、校验失败、无权限、跨租户、资源不存在、数据库约束冲突。创建接口测试相同幂等键的串行与并发重放以及同键不同请求体更新接口让两个客户端持有同一版本确认后提交者收到 409。列表测试边界游标、相同排序值、最大页大小和非法排序字段。核心 API 至此具备可供真实客户端使用的语义。下一篇转到 React 页面把服务端状态、表单状态、路由和错误呈现分开管理并用生成或手写的类型化客户端完成联调。参考来源RFC 9110HTTP 语义OpenAPI SpecificationMicrosoft REST API GuidelinesPostgreSQL显式锁与并发控制 觉得有用就点个赞 收藏方便回头查阅有疑问直接在评论区留言我看到都会回。 本文属于《全栈项目从 0 到 1 实战》系列持续更新关注不迷路。 文章里的代码都能直接跑。想要可直接 clone 的完整工程 配套部署脚本 / 踩坑清单评论一声或发邮件到cj2664qq.com我免费发你。如果你正好在做类似系统、或有工程化难题想找人做也欢迎邮件聊一句——我按实际情况评估能落地的就接单或出方案。评论和邮件都能直接找到我不用跳别的平台。

相关新闻