接口类,需要指定入参类型,出参类型

ThinkPHP6 短信发送工厂模式实现方案

根据你的需求,我设计了一套符合 接口规范 + 工厂模式 的短信发送方案,统一方法名和返回格式,支持动态切换腾讯云 / 阿里云接口,结构清晰且易于扩展。

一、目录结构(遵循 TP6 规范)

plaintext
 
 
app/
├── common/
│   └── lib/
│       ├── sms/
│       │   ├── contract/      # 接口契约
│       │   │   └── SmsInterface.php  # 短信接口规范
│       │   ├── dto/           # 数据传输对象(统一返回格式)
│       │   │   └── SmsResult.php     # 短信返回结构体
│       │   ├── impl/          # 第三方实现
│       │   │   ├── AliyunSms.php     # 阿里云短信实现
│       │   │   └── TencentSms.php    # 腾讯云短信实现
│       │   └── SmsFactory.php        # 短信工厂类
├── config/
│   └── sms.php                       # 短信配置文件
└── controller/
    └── TestController.php            # 测试控制器
 

image

 

二、核心实现代码

1. 接口契约(统一方法规范)

app/common/lib/sms/contract/SmsInterface.php
php
 
 
<?php
namespace app\common\lib\sms\contract;

use app\common\lib\sms\dto\SmsResult;

/**
 * 短信发送接口规范
 * 所有第三方短信实现必须遵循此接口
 */
interface SmsInterface
{
    /**
     * 发送短信
     * @param string $phone 手机号(支持单个号码)
     * @param string $templateId 模板ID
     * @param array $templateParams 模板参数(键值对)
     * @return SmsResult 统一返回结构体
     */
    public function send(string $phone, string $templateId, array $templateParams): SmsResult;
}
 

2. 统一返回结构体(类似 Go 的 struct)

app/common/lib/sms/dto/SmsResult.php
php
 
 
<?php
namespace app\common\lib\sms\dto;

/**
 * 短信发送统一返回结构体
 * 规范所有第三方返回格式,便于上层处理
 */
class SmsResult
{
    // 状态码:200=成功,其他=失败
    private int $code;
    
    // 业务消息
    private string $message;
    
    // 第三方原始响应数据(便于调试)
    private array $raw;
    
    // 自定义扩展数据(如短信ID、回执ID等)
    private array $extra = [];

    public function __construct(int $code, string $message, array $raw = [], array $extra = [])
    {
        $this->code = $code;
        $this->message = $message;
        $this->raw = $raw;
        $this->extra = $extra;
    }

    // 快捷创建成功结果
    public static function success(string $message = '发送成功', array $raw = [], array $extra = []): self
    {
        return new self(200, $message, $raw, $extra);
    }

    // 快捷创建失败结果
    public static function fail(string $message = '发送失败', array $raw = [], array $extra = []): self
    {
        return new self(500, $message, $raw, $extra);
    }

    // getter方法(只读,确保数据不可篡改)
    public function getCode(): int
    {
        return $this->code;
    }

    public function getMessage(): string
    {
        return $this->message;
    }

    public function getRaw(): array
    {
        return $this->raw;
    }

    public function getExtra(): array
    {
        return $this->extra;
    }

    // 转为数组(便于接口返回)
    public function toArray(): array
    {
        return [
            'code' => $this->code,
            'message' => $this->message,
            'extra' => $this->extra,
            'raw' => app()->isDebug() ? $this->raw : [], // 调试模式才返回原始数据
        ];
    }
}
 

3. 第三方实现 - 阿里云短信

app/common/lib/sms/impl/AliyunSms.php
php
 
 
<?php
namespace app\common\lib\sms\impl;

use app\common\lib\sms\contract\SmsInterface;
use app\common\lib\sms\dto\SmsResult;
use AlibabaCloud\Client\AlibabaCloud;
use AlibabaCloud\Dyplsapi\Dyplsapi;
use think\facade\Config;

class AliyunSms implements SmsInterface
{
    // 阿里云配置
    private array $config;

    public function __construct()
    {
        // 从配置文件读取阿里云配置
        $this->config = Config::get('sms.providers.aliyun', []);
        
        // 初始化阿里云客户端
        AlibabaCloud::accessKeyClient(
            $this->config['access_key_id'],
            $this->config['access_key_secret']
        )->regionId($this->config['region_id'])
          ->asDefaultClient();
    }

    /**
     * @inheritDoc
     */
    public function send(string $phone, string $templateId, array $templateParams): SmsResult
    {
        try {
            // 调用阿里云短信API
            $result = Dyplsapi::v20170525()
                ->sendSms()
                ->withPhoneNumbers($phone)
                ->withSignName($this->config['sign_name'])
                ->withTemplateCode($templateId)
                ->withTemplateParam(json_encode($templateParams, JSON_UNESCAPED_UNICODE))
                ->request();

            $rawData = $result->toArray();

            // 解析阿里云返回结果,转为统一格式
            if ($rawData['Code'] === 'OK') {
                return SmsResult::success('阿里云短信发送成功', $rawData, [
                    'biz_id' => $rawData['BizId'],
                    'request_id' => $rawData['RequestId']
                ]);
            } else {
                return SmsResult::fail(
                    "阿里云短信发送失败:{$rawData['Message']}",
                    $rawData,
                    ['error_code' => $rawData['Code']]
                );
            }
        } catch (\Exception $e) {
            return SmsResult::fail(
                "阿里云短信异常:{$e->getMessage()}",
                ['exception' => $e->getMessage(), 'line' => $e->getLine()],
                ['error_code' => $e->getCode()]
            );
        }
    }
}
 

4. 第三方实现 - 腾讯云短信

app/common/lib/sms/impl/TencentSms.php
php
 
 
<?php
namespace app\common\lib\sms\impl;

use app\common\lib\sms\contract\SmsInterface;
use app\common\lib\sms\dto\SmsResult;
use Qcloud\Sms\SmsSingleSender;
use think\facade\Config;

class TencentSms implements SmsInterface
{
    // 腾讯云配置
    private array $config;

    public function __construct()
    {
        // 从配置文件读取腾讯云配置
        $this->config = Config::get('sms.providers.tencent', []);
    }

    /**
     * @inheritDoc
     */
    public function send(string $phone, string $templateId, array $templateParams): SmsResult
    {
        try {
            // 腾讯云短信要求参数为数组格式(按模板顺序)
            $params = array_values($templateParams);
            
            // 初始化腾讯云短信客户端
            $sender = new SmsSingleSender(
                $this->config['app_id'],
                $this->config['app_key']
            );

            // 调用腾讯云短信API
            $result = $sender->sendWithParam(
                $this->config['nation_code'], // 国家码(如86)
                $phone,
                $templateId,
                $params,
                $this->config['sign_name'], // 签名
                '', // 扩展码(可选)
                ''  // 延伸码(可选)
            );

            $rawData = json_decode($result, true) ?: [];

            // 解析腾讯云返回结果,转为统一格式
            if ($rawData['result'] === 0) {
                return SmsResult::success('腾讯云短信发送成功', $rawData, [
                    'sid' => $rawData['sid'],
                    'fee' => $rawData['fee']
                ]);
            } else {
                return SmsResult::fail(
                    "腾讯云短信发送失败:{$rawData['errmsg']}",
                    $rawData,
                    ['error_code' => $rawData['result']]
                );
            }
        } catch (\Exception $e) {
            return SmsResult::fail(
                "腾讯云短信异常:{$e->getMessage()}",
                ['exception' => $e->getMessage(), 'line' => $e->getLine()],
                ['error_code' => $e->getCode()]
            );
        }
    }
}
 

5. 短信工厂类(动态创建实例)

app/common/lib/sms/SmsFactory.php
php
 
 
<?php
namespace app\common\lib\sms;

use app\common\lib\sms\contract\SmsInterface;
use think\facade\Config;
use InvalidArgumentException;

class SmsFactory
{
    /**
     * 动态创建短信实例
     * @param string $driver 驱动名称(aliyun/tencent)
     * @return SmsInterface
     * @throws InvalidArgumentException
     */
    public static function make(string $driver = ''): SmsInterface
    {
        // 若未指定驱动,使用配置文件中的默认驱动
        $driver = $driver ?: Config::get('sms.default', 'aliyun');
        
        // 映射驱动到具体实现类
        $driverMap = [
            'aliyun' => \app\common\lib\sms\impl\AliyunSms::class,
            'tencent' => \app\common\lib\sms\impl\TencentSms::class,
        ];

        // 验证驱动是否支持
        if (!isset($driverMap[$driver])) {
            throw new InvalidArgumentException("不支持的短信驱动:{$driver}");
        }

        $className = $driverMap[$driver];
        
        // 检查类是否存在并实现接口
        if (!class_exists($className) || !is_subclass_of($className, SmsInterface::class)) {
            throw new InvalidArgumentException("短信驱动类 {$className} 未实现 SmsInterface 接口");
        }

        // 创建实例(支持依赖注入)
        return app()->make($className);
    }
}
 

6. 配置文件

config/sms.php
php
 
 
<?php
return [
    // 默认短信驱动(aliyun/tencent)
    'default' => env('SMS_DEFAULT', 'aliyun'),

    // 第三方短信配置
    'providers' => [
        // 阿里云短信配置
        'aliyun' => [
            'access_key_id' => env('ALIYUN_SMS_ACCESS_KEY_ID', ''),
            'access_key_secret' => env('ALIYUN_SMS_ACCESS_KEY_SECRET', ''),
            'region_id' => env('ALIYUN_SMS_REGION_ID', 'cn-hangzhou'),
            'sign_name' => env('ALIYUN_SMS_SIGN_NAME', ''), // 短信签名
        ],

        // 腾讯云短信配置
        'tencent' => [
            'app_id' => env('TENCENT_SMS_APP_ID', ''),
            'app_key' => env('TENCENT_SMS_APP_KEY', ''),
            'nation_code' => env('TENCENT_SMS_NATION_CODE', '86'), // 国家码
            'sign_name' => env('TENCENT_SMS_SIGN_NAME', ''), // 短信签名
        ],
    ],
];
 

7. 环境变量配置(.env)

env
 
 
# 短信配置
SMS_DEFAULT=aliyun # 默认驱动

# 阿里云短信
ALIYUN_SMS_ACCESS_KEY_ID=your_access_key_id
ALIYUN_SMS_ACCESS_KEY_SECRET=your_access_key_secret
ALIYUN_SMS_SIGN_NAME=你的短信签名

# 腾讯云短信
TENCENT_SMS_APP_ID=your_app_id
TENCENT_SMS_APP_KEY=your_app_key
TENCENT_SMS_SIGN_NAME=你的短信签名
 

三、使用示例(控制器调用)

app/controller/TestController.php
php
 
 
<?php
namespace app\controller;

use app\common\lib\sms\SmsFactory;
use app\common\lib\sms\dto\SmsResult;
use think\facade\Log;
use think\response\Json;

class TestController
{
    /**
     * 发送短信示例
     * @return Json
     */
    public function sendSms(): Json
    {
        try {
            // 1. 通过工厂类创建短信实例(动态切换驱动)
            // $sms = SmsFactory::make('tencent'); // 手动指定腾讯云
            $sms = SmsFactory::make(); // 使用默认驱动

            // 2. 调用统一的send方法发送短信
            $phone = '13800138000'; // 接收手机号
            $templateId = 'SMS_123456789'; // 短信模板ID
            $templateParams = [
                'code' => rand(100000, 999999), // 验证码
                'expire' => '5' // 有效期(分钟)
            ];

            /** @var SmsResult $result */
            $result = $sms->send($phone, $templateId, $templateParams);

            // 3. 统一处理返回结果
            if ($result->getCode() === 200) {
                Log::info("短信发送成功:手机号={$phone},回执ID={$result->getExtra()['biz_id'] ?? ''}");
                return json($result->toArray(), 200);
            } else {
                Log::error("短信发送失败:手机号={$phone},错误信息={$result->getMessage()},原始数据=" . json_encode($result->getRaw()));
                return json($result->toArray(), 500);
            }
        } catch (\Exception $e) {
            Log::error("短信发送异常:{$e->getMessage()}", ['trace' => $e->getTraceAsString()]);
            return json([
                'code' => 500,
                'message' => '短信发送异常:' . $e->getMessage()
            ], 500);
        }
    }
}
 

四、依赖安装

需要安装对应第三方 SDK:
bash
 
 
# 阿里云SDK
composer require alibabacloud/dyplsapi-20170525

# 腾讯云SDK
composer require qcloudsms/qcloudsms_php
 

五、核心特性说明

  1. 接口规范约束:所有第三方实现必须遵循 SmsInterface,确保方法名(send)和参数格式统一
  2. 统一返回格式:通过 SmsResult DTO 类,规范返回结构(类似 Go 的 struct),上层无需关心第三方差异
  3. 工厂模式动态切换:通过 SmsFactory::make() 可快速切换驱动,无需修改业务代码
  4. 配置分离:敏感配置放在环境变量,支持多环境部署
  5. 调试友好:调试模式下返回第三方原始响应,便于问题排查
  6. 扩展性强:新增短信服务商(如华为云、极光短信)时,只需:
    • 新建实现类并实现 SmsInterface
    • 在工厂类的 driverMap 中添加映射
    • 在配置文件中添加对应配置

六、注意事项

  1. 需先在对应云服务商控制台完成短信签名和模板的审核
  2. 确保环境变量中配置了正确的 AccessKey、AppID 等敏感信息
  3. 生产环境建议关闭调试模式(app_debug=false),避免泄露原始响应数据
  4. 可根据实际需求扩展 SmsInterface(如添加批量发送、短信回执查询等方法)
posted @ 2025-11-06 09:10  猫猫客服  阅读(10)  评论(0)    收藏  举报