事务中调用第三方,事务失败回滚,第三方成功

事务中调用第三方,事务失败回滚,第三方成功

✅ 原则:

  • 不在事务中调用不可回滚接口!
  • 把“第三方调用”设计为可重试的异步任务
  • 用数据库持久化任务(避免宕机丢失);
  • 异步任务中记录调用结果,避免重复调用;
  • 调用失败可定时重试 + 超过次数报警。

✅ ThinkPHP 实现方案(示例)

1️⃣ 创建一张表:

third_party_tasks

CREATE TABLE `third_party_tasks` (
  `id` INT AUTO_INCREMENT PRIMARY KEY,
  `type` VARCHAR(50) NOT NULL, -- 接口类型,如 send_sms, push_crm
  `payload` TEXT NOT NULL, -- JSON 格式参数
  `status` TINYINT NOT NULL DEFAULT 0, -- 0 待处理,1 成功,2 失败
  `retry_count` INT DEFAULT 0,
  `created_at` DATETIME,
  `updated_at` DATETIME
) ENGINE=InnoDB;

2️⃣ 在本地事务中仅写入任务:

use think\facade\Db;
use think\facade\Queue;

Db::startTrans();
try {
    // 1. 写主业务
    Db::table('user')->insert([
        'name' => '张三',
        // ...
    ]);

    // 2. 写任务表(不要直接发第三方)
    Db::table('third_party_tasks')->insert([
        'type' => 'send_sms',
        'payload' => json_encode([
            'phone' => '13800000000',
            'content' => '欢迎注册'
        ]),
        'status' => 0,
        'created_at' => date('Y-m-d H:i:s'),
        'updated_at' => date('Y-m-d H:i:s'),
    ]);

    Db::commit();

} catch (\Exception $e) {
    Db::rollback();
    throw $e;
}

3️⃣ 写异步任务消费逻辑(定时任务或队列)

// app\command\ProcessThirdParty.php

namespace app\command;

use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Db;

class ProcessThirdParty extends Command
{
    protected function configure()
    {
        $this->setName('process:thirdparty')->setDescription('处理第三方任务');
    }

    protected function execute(Input $input, Output $output)
    {
        $tasks = Db::table('third_party_tasks')
            ->where('status', 0)
            ->limit(10)
            ->select();

        foreach ($tasks as $task) {
            $data = json_decode($task['payload'], true);
            $success = false;

            try {
                // 根据 type 调用对应接口
                switch ($task['type']) {
                    case 'send_sms':
                        $res = sendToThirdPartySms($data['phone'], $data['content']);
                        $success = $res['code'] == 200;
                        break;
                    // 其他类型...
                }
            } catch (\Exception $e) {
                $success = false;
            }

            Db::table('third_party_tasks')
                ->where('id', $task['id'])
                ->update([
                    'status' => $success ? 1 : 2,
                    'retry_count' => $task['retry_count'] + 1,
                    'updated_at' => date('Y-m-d H:i:s'),
                ]);
        }
    }
}

4️⃣ 调用第三方接口方法(可用 Curl 或 Guzzle)

function sendToThirdPartySms($phone, $content)
{
    // 例:伪代码调用第三方 API
    $url = 'https://third.api/send';
    $response = json_decode(file_get_contents($url . "?phone={$phone}&content=" . urlencode($content)), true);
    return $response;
}

5️⃣ 设置定时任务(每分钟跑一次)

* * * * * php think process:thirdparty >> /tmp/third.log 2>&1

✅ 补充建议

  • 可为任务表加唯一约束,防止重复写入;
  • 增加 max_retry 字段,超过次数后报警;
  • 使用队列(如 Redis 队列)也可以代替定时任务;
  • 后台加任务状态展示界面,便于排查和手动补发。

✅ 总结

项目 当前方式 改进方式
第三方接口 事务中直接调用 写入任务表后异步调用
接口是否可撤销 ❌ 否 ✅ 异步前保证事务成功
数据一致性保障 ❌ 低 ✅ 最终一致性、可重试
异常恢复能力 ❌ 弱 ✅ 有补偿、可监控

封装一个完整的ThirdPartyService

✅ 设计目标

  • 支持多类型第三方调用(如发送短信、同步CRM等)
  • 事务中写入任务,不直接调用第三方
  • 异步执行,记录调用结果,可重试
  • 统一入口 ThirdPartyService::dispatch($type, $payload)

** 文件结构建议(ThinkPHP 6)**

app/
├── common/
│   └── service/
│       └── ThirdPartyService.php      ✅ 统一调度服务类
│
├── model/
│   └── ThirdPartyTask.php             ✅ 任务模型
│
├── command/
│   └── ProcessThirdParty.php          ✅ 定时处理命令

✅ 1. 数据表:third_party_tasks

CREATE TABLE `third_party_tasks` (
  `id` INT AUTO_INCREMENT PRIMARY KEY,
  `type` VARCHAR(50) NOT NULL,
  `payload` TEXT NOT NULL,
  `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待处理,1成功,2失败',
  `retry_count` INT DEFAULT 0,
  `max_retry` INT DEFAULT 3,
  `error_message` TEXT,
  `created_at` DATETIME,
  `updated_at` DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

✅ 2. 模型类:

app/model/ThirdPartyTask.php

namespace app\model;

use think\Model;

class ThirdPartyTask extends Model
{
    protected $table = 'third_party_tasks';
    protected $autoWriteTimestamp = true;

    protected $createTime = 'created_at';
    protected $updateTime = 'updated_at';
}

✅ 3. 服务类:

app/common/service/ThirdPartyService.php

namespace app\common\service;

use app\model\ThirdPartyTask;

class ThirdPartyService
{
    /**
     * 调度第三方接口任务
     * @param string $type 接口类型标识,如 send_sms、push_crm
     * @param array $payload 参数数组
     * @param int $maxRetry 最大重试次数
     */
    public static function dispatch(string $type, array $payload, int $maxRetry = 3)
    {
        return ThirdPartyTask::create([
            'type' => $type,
            'payload' => json_encode($payload, JSON_UNESCAPED_UNICODE),
            'status' => 0,
            'retry_count' => 0,
            'max_retry' => $maxRetry,
        ]);
    }

    /**
     * 执行一个任务(可用于队列或定时任务)
     */
    public static function execute(ThirdPartyTask $task)
    {
        $data = json_decode($task->payload, true);
        $success = false;
        $error = '';

        try {
            switch ($task->type) {
                case 'send_sms':
                    $success = self::sendSms($data);
                    break;

                case 'push_crm':
                    $success = self::pushToCRM($data);
                    break;

                default:
                    throw new \Exception("未知任务类型:{$task->type}");
            }
        } catch (\Throwable $e) {
            $error = $e->getMessage();
        }

        $task->retry_count++;
        $task->status = $success ? 1 : 2;
        $task->error_message = $success ? null : $error;
        $task->save();
    }

    // 示例:发送短信
    private static function sendSms(array $data): bool
    {
        // 模拟调用第三方短信接口
        if (empty($data['phone']) || empty($data['content'])) {
            throw new \Exception("缺少短信参数");
        }

        // 伪代码:curl_post('https://api.xxx.com/sms', $data);
        return true;
    }

    // 示例:推送CRM
    private static function pushToCRM(array $data): bool
    {
        // 模拟调用 CRM 接口
        if (empty($data['user_id'])) {
            throw new \Exception("CRM 参数无效");
        }

        return true;
    }
}

✅ 4. 定时任务:

app/command/ProcessThirdParty.php

namespace app\command;

use think\console\Command;
use think\console\Input;
use think\console\Output;
use app\model\ThirdPartyTask;
use app\common\service\ThirdPartyService;

class ProcessThirdParty extends Command
{
    protected function configure()
    {
        $this->setName('process:thirdparty')->setDescription('处理第三方任务队列');
    }

    protected function execute(Input $input, Output $output)
    {
        $tasks = ThirdPartyTask::where('status', 0)
            ->where('retry_count', '<', \Db::raw('max_retry'))
            ->limit(10)
            ->select();

        foreach ($tasks as $task) {
            ThirdPartyService::execute($task);
            $output->writeln("处理任务 #{$task->id} 状态:{$task->status}");
        }
    }
}

✅ 5. 调用示例(业务中使用)

use think\facade\Db;
use app\common\service\ThirdPartyService;

Db::startTrans();
try {
    Db::table('user')->insert([
        'name' => '张三',
    ]);

    // 写入异步任务
    ThirdPartyService::dispatch('send_sms', [
        'phone' => '13800000000',
        'content' => '欢迎加入!'
    ]);

    Db::commit();
} catch (\Exception $e) {
    Db::rollback();
    throw $e;
}

✅ 6. 定时任务配置(每分钟执行)

* * * * * php think process:thirdparty >> /tmp/tptask.log 2>&1

✅ 效果回顾

功能点 实现方式
第三方接口避免事务污染 ✅ dispatch 写入任务表
支持多类型任务分发 ✅ switch-case 结构可扩展
支持重试、失败记录 ✅ retry_count + status
可观察/监控任务执行情况 ✅ 可查数据库任务状态
避免重复调用 ✅ 可扩展幂等逻辑
posted @ 2025-07-07 15:05  刘俊涛的博客  阅读(31)  评论(0)    收藏  举报