ReturnOps Mini 是一个刻意控制规模、但保留真实工程风险的多租户退货退款 SaaS。它不是生产级电商平台,也不是“展示 AI 能写多少代码”的样板,而是一个用于训练工程判断力的小型完整产品。
这个仓库从 commerce-orchestrator 与 administrative-orchestrator 中抽取了最值得学习的可靠性问题:租户隔离、角色权限、状态机、乐观并发控制、幂等、Outbox、外部副作用、结果未知、Webhook 去重、对账、迁移与审计。与此同时,它主动删除了 DBOS、Kafka、Redis、通用工作流引擎、CQRS 等会遮蔽基本机制的基础设施。
训练目标不是逐行背下全部代码,而是达到下面的能力:
- 能画出完整状态与数据流;
- 能说清每条核心不变量由什么代码和数据库约束保证;
- 能在需要的 answer-free probe 中先给出可检验的失败路径预测;
- 能判断一个“测试通过”的结论到底证明了什么、没有证明什么;
- 能在 AI 生成修改后判断影响半径、验证方式与回滚风险;
- 能处理“外部系统实际上成功,但本系统不知道”的不确定现实。
如果完成训练后,你仍然只能说“AI review 说没问题”,那这个项目的训练目标没有达成。最终目标是:AI 主要负责搜索、生成、质疑和制造故障,你本人负责定义正确性、选择证据并做最终判断。
一个商户团队用 ReturnOps Mini 处理退货与退款。系统有四类角色:
customer_service:创建退货、审核退货资格;warehouse:确认收货、检查商品;finance:批准退款、处理不确定结果、完成对账与关闭;admin:租户管理角色;它可以执行部分业务动作,但不是“万能绕过所有规则”的超级用户。
正常业务状态:
REQUESTED
-> AUTHORIZED
-> RECEIVED
-> INSPECTED
-> REFUND_APPROVED
-> REFUND_PENDING
-> REFUNDED
-> RECONCILED
-> CLOSED
外部退款执行可能进入异常分支:
REFUND_PENDING
-> REFUND_UNKNOWN
-> NEEDS_RECONCILIATION
-> REFUNDED
其中最重要的状态是 REFUND_UNKNOWN。一次 HTTP 超时只能证明“本系统没收到确定回复”,不能证明支付方没有执行退款。如果此时自动重新 POST,很可能造成重复退款。因此核心规则是:未知结果不能盲目重试,必须通过 Webhook 或权威查询重新获得事实。
完整清单见 docs/invariants.md。最重要的规则包括:
- 每条业务记录只属于一个 Organization。
- 用户不能读取或修改其他 Organization 的退货记录。
- 跨租户对象 ID 对调用者表现为
404。 - 业务状态只能通过声明过的状态机转换。
- 并发状态写入必须使用
version的 compare-and-swap。 - 退款金额必须大于 0,且不能超过申请金额。
- 高额退款需要两个不同的财务用户对相同金额批准。
- 一个退款意图使用稳定的 provider idempotency key。
- 外部结果未知时禁止自动再次发起退款。
REFUNDED必须由支付方证据产生。- 同一个 Webhook event 最多影响一次业务状态。
CLOSED与REJECTED为终态。
审查 AI 代码时,首先检查“这次改动碰到了哪些不变量”,而不是先看变量名或代码风格。
浏览器 / API Client
|
v
FastAPI API ---- 认证 + 租户成员关系
|
+---- Return Service ---- PostgreSQL
| | | ReturnCase / Approval / Audit
| | | IdempotencyRecord
| +---- Outbox -------| OutboxEvent
|
+---- Payment Webhook ------| WebhookReceipt
|
Worker <---- FOR UPDATE SKIP LOCKED-+
|
v
Fake Payment Provider(独立进程 + SQLite)
|
+---- 同步 HTTP 响应
+---- Webhook
+---- 用于对账的权威查询
本项目故意不使用 DBOS、Kafka、Redis、CQRS、Event Sourcing 或通用工作流引擎。原因不是这些技术不好,而是当前训练目标是看清:哪一步是数据库事实,哪一步只是计划执行,哪一步已经越过网络边界,哪一步只能得到未知,以及哪一个机制真正阻止并发重复写入。
src/returnops/
api/ HTTP 边界、认证与依赖注入
domain/ 状态、角色、转换规则、不变量
services/
returns.py 核心业务动作 + version CAS
idempotency.py 请求 replay / conflict / 并发竞争
outbox.py durable event、claim、lease、retry
payments.py 外部退款意图、结果与证据
webhooks.py Webhook 去重与成功确认
reconciliation.py UNKNOWN 的权威查询与收敛
worker.py Outbox 消费与网络边界
tenancy.py 用户与 Organization 成员关系
models.py SQLAlchemy 持久化模型
static/index.html 极薄的浏览器操作台
src/fake_payment/
app.py 可故障注入的支付方模拟器
tests/
integration/ 必须依赖真实 PostgreSQL 的并发测试
docs/
invariants.md
failure-lab.md
ai-judgment-training.md
建议第一次阅读顺序:docs/invariants.md → domain/states.py → services/returns.py → services/idempotency.py → services/worker.py → services/payments.py → services/webhooks.py → services/reconciliation.py,最后再回看 model、API 和测试。
需要 Docker 与 Compose v2:
docker compose up --build -d
docker compose run --rm api python -m returnops.seedseed 命令会输出一个 Organization ID 和多种角色的 demo bearer token。打开 http://localhost:8000/,在操作台中粘贴 Organization ID 与对应角色 token 即可按角色推进流程。
服务地址:API http://localhost:8000,API health http://localhost:8000/health,Fake payment http://localhost:8090/health。
快速测试:
pytest -q -m 'not integration'真实 PostgreSQL 并发测试:
export RETURNOPS_TEST_DATABASE_URL='postgresql+psycopg://returnops:returnops@localhost:5432/returnops'
pytest -q -m integration不同测试只能证明不同层面的事情:纯函数单测证明状态机与金额规则;SQLite API 测试证明 HTTP 契约、租户过滤和一般事务行为;PostgreSQL 并发测试证明 unique index 等待、row lock、CAS 竞争等真实语义;fake provider 实验证明 ACK 丢失、重复 Webhook、外部已成功但本地未知;migration round-trip 验证 schema 演进。
“测试全绿”不是完整结论。正确问题应是:这组证据具体证明了哪些不变量?还有哪些状态空间没有覆盖?
完整方法见 docs/ai-judgment-training.md。不要把仓库丢给 AI 后只说“全面 review”。推荐固定使用这个训练循环:
选择一个不变量
↓
你先给出最小线索、初始判断或不确定点
↓
AI 展开候选状态/数据路径、反例或形式化结构
↓
你筛选哪些差异会改变判断,并明确需要什么证据
↓
AI 扮演攻击者制造故障或反例
↓
运行测试 / PostgreSQL / fake provider / migration
↓
比较候选判断与真实结果
↓
修正模型,并记录可复用的失败模式
AI 可以高参与地搜索、生成和形式化;answer-free 只在需要测独立检索或预测时使用。最终决定“是否足够正确”、哪些证据足够、何时应重开的仍然是你。
推荐提示词:
你是我的软件工程导师和候选形式化助手。
本轮只围绕这个不变量:<填入一条 invariant>。
规则:
1. 先让我给出最小线索、初始判断或不确定点;
2. 你可以展开候选结构、反例和失败路径,但必须把它们标成候选,不替我做最终判断;
3. 不要因为测试名、注释或代码结构看起来合理就假设它正确;
4. 要求我区分:已确认事实、推测、风险、需要的证据;
5. 只有在本轮明确训练独立检索或预测时,才切换成只提问、不供答案;
6. 最后指出最多 3 个关键遗漏,并给出验证方式。
每次改动合并前,你本人必须回答:这次修改碰到了哪些不变量?改变了哪些状态、数据库行或外部副作用?最危险的失败点是什么?哪个测试或实验能证明它?如果该证明失败,系统最坏会发生什么?
阶段 A:纯状态判断,只研究 domain/states.py 与状态机测试。
阶段 B:数据库不变量,研究 tenant ownership、金额约束、CAS、高额双审批。
阶段 C:请求语义,研究 Idempotency-Key、replay、conflict、20 并发 caller。
阶段 D:外部现实,研究 worker、fake provider、Webhook、UNKNOWN、reconciliation。
阶段 E:系统演进,修改审批规则、增加字段、做 migration、保持旧数据兼容。
这是训练项目,不是可直接部署到真实金融/电商生产环境。第一版本重点是完整性与可验证性,不覆盖真实 PII 合规、生产密钥管理、复杂税务/多币种/部分退款、SSO、完整前端体验、生产级监控/SLO、多区域容灾。
第一版本本地已验证非 integration 测试与 migration round-trip;真实 PostgreSQL 并发语义由 CI/integration 测试继续验证。第二阶段会专门围绕这些未证明边界做硬化。