
为什么先把状态机定死任务类系统最容易失控的地方不在并发而在状态。一个任务从发布到结算中间要经历领取、提交、审核、打款任意一环的状态定义含糊后面的对账就会出现系统说已结算、用户说没到账这类扯皮。本文不谈业务指标只讲数据建模如何把状态、金额、流水三件事建模清楚让对账变成可自动执行的过程。状态机六态与合法迁移把任务单抽象为六个状态待领取INIT、已领取CLAIMED、已提交SUBMITTED、审核中REVIEWING、已通过APPROVED、已驳回REJECTED。终止态只有 APPROVED 与 REJECTED 两个其余都必须能向前推进。合法迁移用一张二维表约束而不是散落在业务代码里- INIT → CLAIMED领取成功写领取时间、领取人、过期时间- CLAIMED → SUBMITTED提交写提交时间、产物地址- CLAIMED → INIT超时未提交回收库存- SUBMITTED → REVIEWING进入审核队列- REVIEWING → APPROVED / REJECTED终态写审核人与审核时间关键约束任何状态变更必须携带前态校验。更新语句形如 UPDATE ... SET state? WHERE id? AND state?靠影响行数判断是否抢到了这次迁移。这一步能挡掉绝大多数并发重复提交。状态变更要不要留痕要而且要独立成表。task_state_log 记录 task_id、from_state、to_state、operator、occur_time、reason。留痕有两个用处一是申诉时能还原全过程二是统计各环节耗时。有了这张表超时回收也可以做成异步扫描 CLAIMED 且 now expire_time 的记录批量回 INIT并补一条状态日志。回收必须是幂等的——同一条记录被扫到两次不能产生两条日志用 (task_id, from_state, to_state, occur_time_bucket) 做唯一索引即可。金额模型单价、冻结与可用金额建模最容易犯的错是把应得和可提混成一个字段。建议拆成三张表或三个字段- earning_record每单通过后的应得金额含任务快照单价、通过时间不可变- balance账户余额分 available 与 frozen 两个子项- withdraw_order提现单含申请金额、状态、渠道回执通过审核时写 earning_record同时 available amount。发起提现时available - amountfrozen amount。渠道回调成功frozen - amount失败则回滚。顺序不能颠倒。先扣可用再冻结否则失败回滚时会出现可用余额短时间为负的窗口。对账三张表怎么核每日对账跑三个比对1. earning_record 的 amount 求和 与 balance 累计入账 是否一致2. withdraw_order 处于成功态的金额求和 与 渠道侧成功金额 是否一致3. balance 中 frozen 的存量 与 withdraw_order 处于处理中态的金额 是否一致三个比对只要有任何一个出现差额就落一条对账差异记录并告警差异记录要带上具体 task_id 或 order_id方便人工追。幂等与重复提交客户端侧网络抖动导致的重复提交是常态。服务端做两层防护第一层是前文的状态前态校验第二层是提交幂等键由 user_id task_id client_token 生成写入唯一索引重复请求直接返回首次结果。审核侧的重复回调同理用渠道回执号做唯一键。幂等键要落在数据库约束上不能只靠应用层判断——应用层判断在并发下必然有窗口。指标与一条硬约束建模时要把体验约束一并写成可观测指标而不是口头约定。本文涉及的两条- 单任务从领取到提交完成耗时 ≤ 5 分钟- 提交后进入审核到出结果审核耗时 ≤ 5 分钟对应埋点领取成功、提交成功、进入审核、审核完成四个事件按 task_id 串联计算差值P50/P90/P99 分开看。超过阈值的任务自动进人工复核队列同时反哺发布侧——某类任务普遍超时说明任务描述本身写得不清楚。小结状态定死、金额拆分、对账可重跑、幂等落到数据库约束上这四件事做完任务类系统的账基本就清楚了。