
做担保交易最核心的问题其实就是信任。不绕弯子就直接说我自己遇到的场景我在运营一个数码产品和虚拟服务的小型交易社群买卖双方大多互不熟悉经常出现我钱都转了对方直接消失或者货发出去了对方死活不确认的情况。以前全靠人工当中间人每一笔都要私聊对账、截图留证、手动转账累不说还容易出纠纷。后来我花了大半个月业余时间用 Python 和 aiogram 框架在 Telegram 上自己搭了一套担保交易系统把买家付款 → 平台托管 → 卖家发货 → 买家确认 → 平台放款整条链路自动化跑通。目前系统已经稳定处理了大几百笔订单中间踩了不少坑也总结出一套比较成熟的做法。这篇文章就把完整的需求拆解、工程搭建、核心代码和避坑经验全部整理出来给想自己做交易机器人、或者要给社群做交易工具的朋友一个可参考的完整方案。1. 先搞清楚需求担保交易流程到底要管哪些事1.1 为什么机器人适合做担保交易的载体接手这个需求之前我其实也犹豫过要不要直接做成 Web 网页后来对比了一圈还是选了 Telegram Bot。原因有三点都很实际。第一是用户习惯。社群里的买卖双方本来就在 Telegram 里沟通把交易入口放到对话框里用户下单、支付、确认收货全都发生在聊天窗口中不需要再跳转网页、注册账号、记密码整个操作路径短到几乎没有学习成本。实际测试下来四五十岁的卖家也能顺利走完流程。第二是 Bot API 本身很适合做状态型业务。Telegram Bot API 自带的 Inline Keyboard内联键盘、Callback Query 回调、以及官方 Bot API 对消息格式的完整支持刚好能覆盖订单创建、确认支付、买家确认收货这类多步骤 用户触发的场景。配合 aiogram 等框架提供的 FSM有限状态机天然就跟订单状态流转对上了。第三是治理与审计。担保交易说白了就是平台帮双方托管资金最怕出纠纷的时候说不清楚。机器人每一次交互都会留下消息记录和时间戳订单状态每次变更都能写入流水表出现争议时可以直接把完整记录导出而不像人工沟通那样各说各话。1.2 交易状态机把流程变成一张明确的图动手写代码之前我做的第一件事不是建工程而是把交易流程画成一张状态图。这一步看着简单但实际价值非常大后面所有代码都围绕着这张图写省了大量返工。核心状态就六个已创建CREATED、已付款PAID、已发货DELIVERED、已完成CONFIRMED、已退款REFUNDED、争议中DISPUTED。状态之间的迁移方向是严格单向的状态触发动作下一个状态说明CREATED买家支付成功PAID资金进入平台托管账户PAID卖家确认发货DELIVERED卖家填写发货信息DELIVERED买家确认收货CONFIRMED资金释放给卖家PAID / DELIVERED双方或管理员发起退款REFUNDED资金退回买家任意进行中状态产生纠纷DISPUTED由管理员介入仲裁DISPUTED仲裁完成CONFIRMED / REFUNDED终态不可再改这里有一个容易忽略的关键点所有状态迁移都必须做前置状态校验。什么意思就是买家点击确认收货按钮时代码不能只看买家身份对不对还必须校验当前订单真实状态是 DELIVERED否则就直接拒绝。我在系统里所有状态变更的地方统一封装了一个 transition 函数发现状态不匹配就记录异常日志并拒绝操作这套机制后来真的拦截了好几次因为前端按钮重复点击导致的状态错乱。2. 技术选型与工程搭建3步完成机器人初始化2.1 在 BotFather 申请机器人并拿到 Token搭建的第一步是从 BotFather 拿到机器人的身份证。直接在 Telegram 里搜索 BotFather给它发送 /newbot按提示填写机器人显示名称和用户名用户名必须以 bot 结尾比如 my_escrow_bot。创建完成后 BotFather 会返回一个形如 123456789:AAF... 的 HTTP API Token这个 Token 就是后续所有 API 请求的凭证。关于 Token 有两条血泪经验。第一Token 一旦泄露任何人都能完全控制你的机器人所以绝对不能硬编码在代码里也不要提交到 Git 仓库用环境变量或者独立的配置文件加载。第二如果怀疑 Token 泄露去 BotFather 执行 /revoke旧 Token 立刻失效再创建一个新 Token 换上即可不需要重新建机器人。拿到 Token 之后顺手做两个设置一是把 /setprivacy 设为 Disable保证机器人能读取群聊里的完整消息二是用 /setdescription 和 /setabouttext 写清楚机器人的用途这些基础信息会展示在机器人资料页影响用户第一信任感。2.2 项目结构与依赖安装后端语言我选了 Python框架用 aiogram 3.x。选 aiogram 而不是纯 requests 调 Bot API最重要的原因是它把很多繁琐的底层细节都封装好了自动处理偏移量、长轮询与 Webhook 切换、类型化消息对象、以及内置的 FSM 存储。开发效率差距非常大。项目结构我习惯按业务拆分避免所有代码堆在 main.py 里变成一个五千行的巨无霸escrow_bot/ ├── main.py # 入口文件初始化 bot / dispatcher ├── config.py # 读取环境变量与配置 ├── database.py # 数据库连接与会话管理 ├── models.py # 数据模型用户、订单、流水 ├── handlers/ │ ├── __init__.py │ ├── user.py # 用户侧命令与下单逻辑 │ ├── trade.py # 订单流程支付、发货、确认 │ └── admin.py # 管理员后台仲裁、退款、数据统计 ├── services/ │ ├── order_service.py # 订单状态机与核心业务逻辑 │ ├── payment_service.py # 支付回调处理与资金流水 │ └── notify.py # 消息通知封装 └── requirements.txt依赖安装只需要几行pip install aiogram3.4.0 pip install sqlalchemy2.0.25 pip install psycopg2-binary pip install redis数据库我用的 PostgreSQL连接串和 Token 一样放到环境变量里。开发阶段也可以先用 SQLite 顶一下但正式环境建议直接上 PostgreSQL因为担保交易涉及资金并发场景下 PostgreSQL 的行锁和事务特性更可靠后面我会专门讲并发问题。2.3 数据库建表用户、订单、流水三张核心表担保系统的数据模型是我整个项目里最谨慎的部分因为一旦表结构设计错了改起来牵一发动全身。最终我收敛为三张核心表用户表、订单表、资金流水表。CREATE TABLE users ( user_id BIGINT PRIMARY KEY, username TEXT, role TEXT NOT NULL DEFAULT user, -- user / admin rating INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE orders ( order_id BIGINT PRIMARY KEY, buyer_id BIGINT NOT NULL REFERENCES users(user_id), seller_id BIGINT NOT NULL REFERENCES users(user_id), title TEXT NOT NULL, amount NUMERIC(10, 2) NOT NULL CHECK (amount 0), status TEXT NOT NULL DEFAULT CREATED, payment_tx_id TEXT UNIQUE, -- 支付渠道流水号幂等关键 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), paid_at TIMESTAMPTZ, delivered_at TIMESTAMPTZ, confirmed_at TIMESTAMPTZ, dispute_reason TEXT ); CREATE TABLE transactions ( tx_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, order_id BIGINT NOT NULL REFERENCES orders(order_id), user_id BIGINT NOT NULL, amount NUMERIC(10, 2) NOT NULL, tx_type TEXT NOT NULL, -- CHARGE / RELEASE / REFUND created_at TIMESTAMPTZ NOT NULL DEFAULT now() );订单表里的 payment_tx_id 字段加了 UNIQUE 约束这是保证支付幂等的关键后面细说。金额字段统一用 NUMERIC 类型千万别用 FLOAT浮点数在涉及资金计算时的精度问题会给你带来灾难。3. 核心流程实现从下单到放款的完整代码3.1 下单选品创建订单的完整逻辑我先给用户侧实现了两个命令/buy 发起购买/sell 发布出售。实际交易中更常见的模式是卖家提供一个担保链接或者商品编码买家输入后开始走流程。我这里以买家输入商品描述和金额为例。下单的核心逻辑是先确认买家和卖家都存在且不是同一个人然后校验金额是否在系统允许范围内我设的是 1 到 50000最后写入订单表生成带唯一订单号的确认消息发给双方。# handlers/trade.py from aiogram import Router, F from aiogram.types import Message from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from services.order_service import create_order router Router() class OrderStates(StatesGroup): waiting_title State() waiting_seller State() waiting_amount State() router.message(Command(buy)) async def start_buy(message: Message, state: FSMContext): await message.answer(请输入交易商品名称/简要描述) await state.set_state(OrderStates.waiting_title)用户完成描述输入后还需要填写卖家用户名和金额。这里有个设计细节让买家直接输入卖家的用户名而不是从通讯录选择是为了方便系统自动解析 seller_id 并校验卖家是否已注册机器人。如果卖家还没用过机器人我会直接提示请先让卖家向机器人发送 /start。创建订单的 service 层是整个流程的核心# services/order_service.py from database import get_db from models import Order def create_order(buyer_id: int, seller_id: int, title: str, amount: float) - Order: if buyer_id seller_id: raise ValueError(买家和卖家不能是同一人) if amount 0: raise ValueError(金额必须大于 0) with get_db() as db: order Order( buyer_idbuyer_id, seller_idseller_id, titletitle, amountamount, statusCREATED ) db.add(order) db.commit() db.refresh(order) return order订单创建成功后机器人会给双方各发一条消息里面有订单号、金额和状态并给买家展示两个按钮确认支付和取消订单。这里有个很重要的小细节所有键盘回调的 callback_data 都要带上 order_id比如pay:12345这样后端收到回调时就知道该操作哪个订单而不是靠用户自己说。3.2 支付入账与到账确认买家点击确认支付后系统需要引导付款并把资金纳入托管。支付这块我做了两层抽象第一层是接入支付渠道的回调第二层是人工线下转账后的对账确认。实际部署时两种模式可以并存。如果是走线上支付渠道支付网关会通过回调通知机器人该订单已支付。回调处理函数里最重要的就是幂等性检查# services/payment_service.py def handle_payment_callback(order_id: int, tx_id: str, amount: float): with get_db() as db: order db.execute( select(Order).where(Order.order_id order_id) ).scalar_one() # 幂等检查同一支付流水号不能重复入账 existing db.execute( select(Transaction).where(Transaction.tx_id tx_id) ).scalars().first() if existing: return False, 重复回调已忽略 if order.status ! CREATED: return False, f订单状态异常: {order.status} if abs(order.amount - amount) 0.01: return False, 支付金额与订单金额不一致 # 入账并更新订单状态 order.status PAID order.paid_at datetime.now(timezone.utc) order.payment_tx_id tx_id db.add(Transaction(order_idorder.id, user_idorder.buyer_id, amountamount, tx_typeCHARGE)) db.commit() return True, ok为什么必须校验金额和订单状态因为支付回调这种外部请求经常有重试机制网卡一下回调就重复发了如果没有幂等保护买家付一次款系统却入账两次资金对不上就是重大事故。我还在数据库层给 payment_tx_id 加了 UNIQUE 约束就算应用层逻辑漏了数据库也会兜底拒绝重复流水。如果是线下转账模式就需要管理员在后台人工核对转账截图后执行确认到账操作本质上是复用同一套状态变更逻辑只是这一步从自动化变成了管理员触发。3.3 确认收货与担保放款订单状态变为 PAID 后卖家会收到通知点击确认发货按钮填写发货备注比如快递单号或数字商品的交付方式。发货后订单进入 DELIVERED 状态这时系统会给买家推送消息卖家已发货请在确认收到商品后点击【确认收货】。确认收货是整个担保系统的敏感操作因为这一步意味着资金要从托管账户释放给卖家。我在代码里做了双重校验# services/order_service.py def confirm_receipt(order_id: int, user_id: int) - tuple[bool, str]: with get_db() as db: order db.execute( select(Order).where(Order.order_id order_id) ).scalar_one() if order.buyer_id ! user_id: return False, 只有买家可以确认收货 if order.status ! DELIVERED: return False, f当前订单状态为 {order.status}不能确认收货 # 资金释放给卖家 order.status CONFIRMED order.confirmed_at datetime.now(timezone.utc) db.add(Transaction(order_idorder.id, user_idorder.seller_id, amountorder.amount, tx_typeRELEASE)) db.commit() return True, 交易完成资金已释放给卖家这里有一个容易忽略的业务问题如果托管账户里实际余额不足放款就会失败。所以我设计了一个资金托管台账的机制用每次 CHARGE 和 RELEASE 的流水做累计对账。每次放款前先校验托管账户可用余额是否覆盖这笔订单金额余额不足就报警并冻结放款等人工处理。实际运营中确实遇到过因为外部支付渠道延迟导致账面已收但实际未到账的情况这个校验帮我们避免了一次资金透支。3.4 退款与争议处理不是每笔交易都顺顺利利退款和争议处理是担保系统最考验设计能力的地方。我的规则是CREATED 状态下的订单买家可以无条件取消不产生资金变动PAID 或 DELIVERED 状态下如果双方协商一致退款需要卖家在机器人里发起同意退款确认然后系统自动退款给买家如果双方协商不一致任何一方都可以点击发起争议订单状态变为 DISPUTED机器人会自动通知管理员介入。争议处理的人工界面我做得比较克制管理员只需要做两件事查看订单完整时间线谁在什么时候做了什么操作、支付流水、发货备注然后选择仲裁放款或仲裁退款。仲裁后订单进入终态所有按钮全部失效不会再有二次流转。争议处理中我觉得最有用的是时间线功能。我在每次状态变更时都会自动给订单追加一条带有操作者和时间戳的记录管理员只需要在后台输入订单号就能看到完整的操作序列。处理纠纷时这个时间线比双方各执一词的聊天记录有说服力得多。4. 安全与风控担保系统绝对不能省的部分4.1 权限控制与管理员机制担保交易系统手里握着用户的钱权限控制必须做到位。我把用户角色分为三级普通用户、管理员、超级管理员。管理员可以查看与仲裁订单但不能修改资金流水只有超级管理员才能直接操作托管账户。aiogram 里做权限控制最简单的方式是写一个装饰器def admin_required(func): async def wrapper(message: Message, *args, **kwargs): user_id message.from_user.id if not is_admin(user_id): await message.answer(你没有权限执行此操作) return return await func(message, *args, **kwargs) return wrapper同时管理后台不能只靠机器人聊天窗口操作。我另外加了一道操作验证码管理员执行退款、放款等敏感操作时机器人会先在私聊里推送一条确认消息要求管理员输入一段随机验证码后才真正执行。这个措施虽然多了一步但能有效防止管理员账号被盗或者误触操作造成的资金事故。4.2 幂等性与并发处理在线交易系统最怕的就是同一笔操作被触发两次。前面提到支付回调的幂等靠 payment_tx_id 的 UNIQUE 约束兜底但状态变更同样需要防护。比如买家在确认收货的瞬间如果网络不好导致按钮被重复点击两个请求同时进入理论上就可能出现同一笔资金被释放两次。解决方案是事务加行锁。在确认收货的数据库事务里用 SELECT ... FOR UPDATE 先把订单行锁住再检查状态并更新from sqlalchemy import select, update from sqlalchemy.orm import sessionmaker def confirm_receipt_safe(order_id: int, user_id: int): with get_db() as db: # 锁定订单行防止并发修改 order db.execute( select(Order).where(Order.order_id order_id).with_for_update() ).scalar_one() if order.status ! DELIVERED: return False, 重复操作已拦截 # 后续更新操作加上行锁之后第二个并发请求必须等第一个事务提交后才能读取到最新状态此时状态已经变成 CONFIRMED自然就会被拦截。这套机制上线后再也没有出现过重复放款。类似的所有状态变更函数都遵循先锁行、再校验、后更新的原则。4.3 反欺诈与审计日志担保系统天然会被欺诈者盯上最常见的套路有几种我基本都遇到过了。第一个是假付款截图骗卖家发货这个靠线下对账模式根本防不住所以我强烈建议优先接入支付渠道的回调让系统自动确认到账而不是人工看截图。第二个是买家收货后退款即买家先正常确认收货随后跑到支付渠道那边发起争议要求退款导致卖家货发出去了钱却没拿到。这种情况平台很难完全避免但我能做的是在用户注册时记录绑定信息并对短期内频繁发起争议的账号做风控标记。审计日志方面除了 transactions 表保留每一笔资金流水我还把所有敏感操作写进了独立日志表包含操作人、操作类型、订单号、旧状态、新状态、时间戳。就算将来出现纠纷需要追责所有数据都是完整可查的。日志不是用来给用户看的而是用来给平台自己兜底的这个钱绝对不能省。5. 常见问题与避坑实录5.1 部署与运行问题速查表把系统上线过程中踩过的坑按问题类型整理成一张表方便遇到同样问题的朋友直接对照问题表现根本原因解决办法机器人收不到消息或消息延迟Token 错误 / 权限设置问题 / 网络异常检查 Token 是否复制完整确认 BotFather 的 privacy 设为 Disable重启进程看日志长轮询报 409 Conflict多个进程同时使用同一个 Token 轮询只保留一个 polling 进程或全部切换为 WebhookCallbackQuery 点击后没有反应回调处理超时超过 60 秒未回执在 handler 开头立即 answer_callback_query再处理业务逻辑用户重复点击支付/确认按钮导致状态乱缺少状态前置校验所有状态变更函数统一做前置状态校验并在数据库层加行锁支付回调重复入账支付渠道重试回调对支付流水号加 UNIQUE 约束应用层先查重再入库机器人发送消息被限流FloodWait触发了发送频率限制广播类消息限速批量发送控制每 30 秒最多发 30 条左右禁用高并发盲目复读业务数据库连接池耗尽每次操作新建连接未释放用 SQLAlchemy 的 session 上下文管理器统一管理连接生命周期服务器重启后丢失订单状态使用了内存型 FSM 存储生产环境把 FSM 存储切换为 Redis保证状态可恢复5.2 上线后的运营注意事项系统跑起来只是第一步真正稳定运营还需要做一些流程上的补充。第一冷启动阶段的信任问题。新的担保机器人没有口碑用户不敢用这是最现实的问题。我的做法是先在自己的社群里跑内测由我本人做担保前几十笔订单人工陪着走完流程把成功的交易记录沉淀下来形成口碑后再放开。担保系统本质上是在卖平台信用没有信用积累之前功能再完善也很难冷启动。第二客服响应机制。纠纷处理不能全自动背后必须有真人响应。我给管理员群接入了机器人的关键事件通知包括大额订单、争议发起、余额不足等保证管理员能在几分钟内介入。实践证明争议处理速度直接决定了用户对平台的信任度。第三对账习惯。就算有托管台账我仍然要求自己每天凌晨跑一次对账脚本核对当日所有订单的 CHARGE 流水和 RELEASE 流水是否与托管账户实际变动一致。发现问题立即冻结相关订单宁可慢一点也不能错一笔。第四关于支付渠道的兜底策略。支付渠道偶尔会有回调延迟甚至丢单的情况所以我在管理后台加了一个手动补单功能但只有超级管理员有权限操作而且每次补单都会强制记录原因和操作者。这个功能不是为了天天用但真遇到丢单时它是唯一能救急的出口。根据我这段时间实际运营的经验担保交易系统最难的其实不是机器人代码本身而是围绕它建立起来的一整套信任与风控体系。代码写错了可以改用户的信任一旦丢了就很难挽回。所以如果你也准备做类似系统我建议把更多精力放在流程设计的严谨性、资金对账的自动化、以及争议处理的速度上这三件事做好比把界面做得花里胡哨有用得多。最后再分享一个小技巧订单号生成不要用自增主键直接展示给用户而是单独生成一串带校验位的短码比如 E-20250608-7F3K9A 这样的格式。这样用户在群里报订单号时不会看错也不容易暴露平台真实的订单量对运营数据也是一种保护。