瀏覽代碼

Merge remote-tracking branch 'origin/liuzj-1001016-dev' into stable

zqwang 1 年之前
父節點
當前提交
5c7c949dd9

+ 89 - 0
app/Console/Commands/WechatPlatform/KFMessageSend.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace App\Console\Commands\WechatPlatform;
+
+use App\Jobs\WechatPlatform\GZHSendKFMessage;
+use App\Service\Util\Support\Trace\TraceContext;
+use App\Service\WechatPlatform\WechatPlatformConstService;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
+
+
+class KFMessageSend extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'WechatPlatform:KFMessageSend {--pk= : wechat_kf_messages.id}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Execute the console command.
+     */
+    public function handle()
+    {
+        $pk = $this->option('pk');
+        $traceContext = new TraceContext();
+        $now = date('Y-m-d H:i:s');
+        DB::table('wechat_kf_messages')
+            ->where([
+                ['status', '=', WechatPlatformConstService::KF_MESSAGE_STATUS_PRE_SEND],
+                ['is_enabled', '=', 1],
+                ['gzh_ids', '<>', '']
+            ])->whereNotNull('send_at')
+            ->when($pk, function ($query, $pk){
+                return $query->where('id', $pk);
+            }, function ($query) use ($now) {
+                return $query->where('send_at', '<=', $now)
+                    ->where('send_at', '>', date('Y-m-d H:i:s', strtotime('-15 minute')));
+            })->orderBy('id')
+            ->chunk(100, function ($items) use ($traceContext, $now){
+                DB::table('wechat_kf_messages')
+                    ->whereIn('id', $items->pluck('id'))
+                    ->update([
+                        'status'  => WechatPlatformConstService::KF_MESSAGE_STATUS_SENDING,
+                        'updated_at' => $now
+                    ]);
+
+                foreach ($items as $item) {
+                    myLog('KFMessageSend')->info('开始处理消息', [
+                        'message_id' => $item->id,
+                        'traceInfo' => $traceContext->getTraceInfo()
+                    ]);
+                    try {
+                        $gzhIds = explode('#', trim($item->gzh_ids, '#'));
+
+                        foreach ($gzhIds as $gzhId) {
+                            GZHSendKFMessage::dispatch([
+                                'gzhId' => $gzhId,
+                                'messageId' => $item->id,
+                                'traceInfo' => $traceContext->getTraceInfo()
+                            ])->onConnection('queue-redis')
+                                ->onQueue('{duanju_manage}.wechatPlatform.sendKFMessage');
+                        }
+                    }catch (\Exception $exception) {
+                        myLog('KFMessageSend')->error('发送客服消息异常', [
+                            'message_id' => $item->id,
+                            'exceptionMsg' => $exception->getMessage(),
+                            'traceInfo' => $traceContext->getTraceInfo()
+                        ]);
+                    }
+                }
+
+                DB::table('wechat_kf_messages')
+                    ->whereIn('id', $items->pluck('id'))
+                    ->update([
+                        'status'  => WechatPlatformConstService::KF_MESSAGE_STATUS_FINISH,
+                        'updated_at' => $now
+                    ]);
+            });
+    }
+}

+ 45 - 0
app/Console/Commands/WechatPlatform/Test.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace App\Console\Commands\WechatPlatform;
+
+use App\Jobs\WechatPlatform\GZHSendKFMessage;
+use App\Service\Util\Support\Trace\TraceContext;
+use App\Service\WechatPlatform\WechatPlatformConstService;
+use EasyWeChat\Factory;
+use EasyWeChat\Kernel\Messages\Text;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\DB;
+
+
+class Test extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'WechatPlatform:Test';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Execute the console command.
+     */
+    public function handle()
+    {
+        $info = [
+            'gzhId' => 8,
+            'messageId' => 6,
+            'traceInfo' => (new TraceContext())->getTraceInfo()
+        ];
+        $skf = new GZHSendKFMessage($info);
+
+        $skf->handle();
+    }
+}

+ 4 - 0
app/Console/Kernel.php

@@ -33,6 +33,10 @@ class Kernel extends ConsoleKernel
          * 检查短剧剧目拉取任务
          */
         $schedule->command('WechatCheck:GetTaskInfo')->everyTenMinutes();
+        /**
+         * 客服消息发送
+         */
+        $schedule->command('WechatPlatform:KFMessageSend')->everyMinute();
     }
 
     /**

+ 225 - 0
app/Jobs/WechatPlatform/GZHSendKFMessage.php

@@ -0,0 +1,225 @@
+<?php
+
+namespace App\Jobs\WechatPlatform;
+
+use App\Service\Util\Support\Http\HttpRequestService;
+use App\Service\Util\Support\Trace\TraceContext;
+use App\Service\WechatPlatform\GZHSendKFMessageService;
+use App\Service\WechatPlatform\WechatPlatform;
+use EasyWeChat\OfficialAccount\Application;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\DB;
+
+
+class GZHSendKFMessage implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    /**
+     * @var
+     * <pre>
+     * [
+     *  'gzhId' => $gzhId,   // wechat_authorization_infos.id
+     *  'messageId' => $item->id,  // wechat_kf_messages.id
+     *  'traceInfo' => $traceContext->getTraceInfo()  // traceInfo
+     * ]
+     * </pre>
+     */
+    private $info;
+
+
+    /**
+     * @var TraceContext
+     */
+    private $traceContext;
+
+    /**
+     * Create a new job instance.
+     */
+    public function __construct($info)
+    {
+        $this->info = $info;
+    }
+
+    /**
+     * Execute the job.
+     */
+    public function handle(): void
+    {
+        $this->traceContext = TraceContext::newFromParent($this->info['traceInfo']);
+        myLog('KFMessageSend')->info('公众号开始发送客服消息', [
+            'info' => $this->info,
+            'traceInfo' => $this->traceContext->getTraceInfo(),
+        ]);
+
+        $gzh = $this->getGZH();
+        if (!$gzh) return;
+
+        $message = $this->getMessage();
+        if (!$message) return;
+
+
+        $messageContent = collect(\json_decode($message->message_content, true));
+        $messageStr = $messageContent->pluck('text')->join("\n\n");
+
+        $officialAccount = $this->getOfficialAccount($gzh);
+        if (false === $officialAccount) return;
+
+        if ($this->info['isTest'] ?? false) {
+            $openid = $this->info['openid'] ?? '';
+            if (!$openid) {
+                myLog('KFMessageSend')->error('测试回传没有openid', [
+                    'info' => $this->info
+                ]);
+            }
+            GZHSendKFMessageService::sendText($officialAccount, $openid, $messageStr, $this->traceContext);
+        } else {
+            $next_openid = '';
+            $loop = 1;
+            while (true) {
+                if ($loop++ > 10000) {
+                    break;
+                }
+                if (1 == $message->u_type) {
+                    $info = $this->getUserOpenids($officialAccount, $next_openid);
+                    foreach (($info['data']['openid'] ?? []) as $opid) {
+                        GZHSendKFMessageService::sendText($officialAccount, $opid, $messageStr, $this->traceContext);
+                    }
+                    $next_openid = $info['next_openid'] ?? '';
+                    if (!$next_openid) {
+                        break;
+                    }
+                } elseif (2 == $message->u_type) {
+                    $info = $this->getUsersFromUG($gzh->id, $message->ug_id, $next_openid);
+                    foreach (($info['openid'] ?? []) as $opid) {
+                        GZHSendKFMessageService::sendText($officialAccount, $opid, $messageStr, $this->traceContext);
+                    }
+                    $next_openid = $info['next_uid'] ?? '';
+                    if (!$next_openid) {
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    private function getMessage()
+    {
+        $message = DB::table('wechat_kf_messages')
+            ->where('id', $this->info['messageId'])
+            ->first();
+        if (!$message) {
+            myLog('KFMessageSend')->error('消息不存在', [
+                'info' => $this->info,
+                'traceInfo' => $this->traceContext->getTraceInfo(),
+            ]);
+            return false;
+        }
+        if (1 != $message->message_type) {
+            myLog('KFMessageSend')->error('不支持的消息类型', [
+                'info' => $this->info,
+                'traceInfo' => $this->traceContext->getTraceInfo(),
+            ]);
+            return false;
+        }
+        return $message;
+    }
+
+    /**
+     * 拉取公众号粉丝
+     * @param $officialAccount Application
+     */
+    private function getUserOpenids($officialAccount, $next_openid)
+    {
+        $result = $officialAccount->user->list($next_openid);
+
+        if (0 != ($result['errcode'] ?? 0)) {
+            return false;
+        }
+        return $result;
+    }
+
+    /**
+     * 通过用户分群获取
+     * @param $gzhId
+     * @param $ugId
+     * @param $nextUid
+     * @return false|mixed
+     */
+    private function getUsersFromUG($gzhId, $ugId, $nextUid)
+    {
+        $url = config('wechat.ug.url.listUser');
+        $signKey = config('wechat.ug.signKey');
+        $now = time();
+        $res = HttpRequestService::simpleGet($url, [
+            'timestamp' => $now,
+            'sign' => md5($signKey . $now),
+            'gzhId' => $gzhId,
+            'ugId' => $ugId,
+            'nextUid' => $nextUid,
+            'limit' => 1000
+        ]);
+        if ($res) {
+            myLog('KFMessageSend')->debug('通过用户分群获取用户列表', [
+                'gzhId' => $gzhId,
+                'ugId' => $ugId,
+                'res' => $res
+            ]);
+            if (10000 == ($res['code'] ?? 10000)) {
+                return $res;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 获取公众号调用对象
+     * @param $gzh
+     * @return \EasyWeChat\OfficialAccount\Application
+     * @throws \EasyWeChat\Kernel\Exceptions\BadResponseException
+     * @throws \EasyWeChat\Kernel\Exceptions\HttpException
+     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
+     * @throws \Psr\SimpleCache\InvalidArgumentException
+     * @throws \Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface
+     * @throws \Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface
+     * @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
+     * @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
+     * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
+     */
+    private function getOfficialAccount($gzh)
+    {
+        try {
+            return WechatPlatform::buildApplication($gzh)
+                ->officialAccount($gzh->authorizer_appid, $gzh->authorizer_refresh_token);
+        } catch (\Throwable $exception) {
+            myLog('KFMessageSend')->error('获取公众号调用对象失败', [
+                'exceptionMessage' => $exception->getMessage(),
+                'traceInfo' => $this->traceContext->getTraceInfo()
+            ]);
+            return false;
+        }
+    }
+
+    private function getGZH()
+    {
+        $gzh = DB::table('wechat_authorization_infos as a')
+            ->join('wechat_open_platform_infos as b', 'a.component_appid', 'b.app_id')
+            ->where([
+                ['a.id', '=', $this->info['gzhId']],
+                ['a.is_enabled', '=', 1],
+                ['b.is_enabled', '=', 1]
+            ])->select('a.id', 'a.authorizer_appid', 'a.authorizer_refresh_token',
+                'b.app_id', 'b.secret', 'b.token', 'b.aes_key')
+            ->first();
+        if (!$gzh) {
+            myLog('KFMessageSend')->error('公众号不可用', [
+                'traceInfo' => $this->traceContext->getTraceInfo(),
+            ]);
+        }
+        return $gzh;
+    }
+}

+ 6 - 3
app/Service/Util/Support/Http/HttpRequestService.php

@@ -13,7 +13,7 @@ class HttpRequestService
      * @param $postMessage 请求体
      * @return array
      */
-    public static function simplePost($url, $postMessage) {
+    public static function simplePost($url, $postMessage, $traceContext = null) {
         $client = new Client(['timeout' => 10]);
         try {
             $res = $client->post(
@@ -32,12 +32,13 @@ class HttpRequestService
                 'url' => $url,
                 'postJson' => $postMessage,
                 'exceptionMessage' => $exception->getMessage(),
+                'traceInfo' => is_null($traceContext) ? '' : $traceContext->getTraceInfo,
             ]);
         }
         return false;
     }
 
-    public static function post($url, $options) {
+    public static function post($url, $options, $traceContext = null) {
         $client = new Client(['timeout' => 10]);
         try {
             $res = $client->request('POST', $url, $options);
@@ -52,12 +53,13 @@ class HttpRequestService
                 'url' => $url,
                 'postOptions' => $options,
                 'exceptionMessage' => $exception->getMessage(),
+                'traceInfo' => is_null($traceContext) ? '' : $traceContext->getTraceInfo,
             ]);
         }
         return false;
     }
 
-    public static function simpleGet($url, $query) {
+    public static function simpleGet($url, $query, $traceContext = null) {
         $client = new Client(['timeout' => 10]);
         try {
             $response = $client->get($url, [
@@ -74,6 +76,7 @@ class HttpRequestService
                 'url' => $url,
                 'query' => $query,
                 'exceptionMessage' => $exception->getMessage(),
+                'traceInfo' => is_null($traceContext) ? '' : $traceContext->getTraceInfo,
             ]);
         }
         return false;

+ 4 - 0
app/Service/Util/Support/Http/WechatURL.php

@@ -16,4 +16,8 @@ class WechatURL
     public const vod_gettask = 'https://api.weixin.qq.com/wxa/sec/vod/gettask?access_token=';
     // 获取媒资列表
     public const vod_listmedia = 'https://api.weixin.qq.com/wxa/sec/vod/listmedia?access_token=';
+    /**
+     * 客服接口-发消息
+     */
+    public const send_custom_message = 'https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=';
 }

+ 41 - 0
app/Service/WechatPlatform/GZHSendKFMessageService.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Service\WechatPlatform;
+
+use App\Service\Util\Support\Http\HttpRequestService;
+use App\Service\Util\Support\Http\WechatURL;
+use EasyWeChat\OpenPlatform\Application;
+use Illuminate\Support\Facades\Cache;
+
+class GZHSendKFMessageService
+{
+    /**
+     * 发送文本客服消息
+     * @param $officialAccount \EasyWeChat\OfficialAccount\Application
+     * @param $openid
+     * @param $content
+     * @param $traceContext
+     * @return bool
+     */
+    public static function sendText($officialAccount, $openid, $content, $traceContext) {
+        try {
+            $res = $officialAccount->customer_service->message($content)
+                ->to($openid)->send();
+            myLog('KFMessageSend')->debug('客服消息发送结果:', [
+                'res' => $res,
+                'traceInfo' => $traceContext->getTraceInfo()
+            ]);
+            return true;
+        } catch (\Throwable $exception) {
+            myLog('KFMessageSend')->error('发送客服消息失败', [
+                'openid' => $openid, 'content' => $content,
+                'exceptionMsg' => $exception->getMessage(),
+                'expTrace' => $exception->getTraceAsString(),
+                'traceInfo' => $traceContext->getTraceInfo()
+            ]);
+            return false;
+        }
+    }
+
+
+}

+ 33 - 0
app/Service/WechatPlatform/WechatPlatform.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace App\Service\WechatPlatform;
+
+use EasyWeChat\Factory;
+use EasyWeChat\OpenPlatform\Application;
+use Illuminate\Support\Facades\Cache;
+
+class WechatPlatform
+{
+    public static function buildApplication($componentInfo) {
+        $config = [
+            'app_id' => $componentInfo->app_id, // 开放平台账号的 appid
+            'secret' => $componentInfo->secret,   // 开放平台账号的 secret
+            'token' => $componentInfo->token,  // 开放平台账号的 token
+            'aes_key' => $componentInfo->aes_key,   // 明文模式请勿填写 EncodingAESKey
+        ];
+
+        $app = Factory::openPlatform(array_merge($config, config('easywechat')));
+        $app->rebind('cache',Cache::store('redis'));
+        return $app;
+    }
+
+    /**
+     * @param $app Application
+     * @param $appid
+     * @param $refreshToken
+     * @return \EasyWeChat\OfficialAccount\Application
+     */
+    public static function officialAccount($app, $appid, $refreshToken) {
+        return $app->officialAccount($appid, $refreshToken);
+    }
+}

+ 36 - 0
app/Service/WechatPlatform/WechatPlatformConstService.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Service\WechatPlatform;
+
+class WechatPlatformConstService
+{
+    /**
+     * 客服消息状态:待发送
+     */
+    public const KF_MESSAGE_STATUS_PRE_SEND=1;
+    /**
+     * 客服消息状态:发送中
+     */
+    public const KF_MESSAGE_STATUS_SENDING=2;
+    /**
+     * 客服消息状态:发送完成
+     */
+    public const KF_MESSAGE_STATUS_FINISH=3;
+    /**
+     * 客服消息状态:已停止
+     */
+    public const KF_MESSAGE_STATUS_STOP=4;
+    /**
+     * 客服消息状态-映射表
+     */
+    public const KF_MESSAGE_STATUS_MAPPER = [
+        '1' => '待发送', '2'  => '发送中', '3' => '发送完毕', '4' => '已停止'
+    ];
+
+    /**
+     * 客服消息类型:纯文本
+     */
+    public const KF_MESSAGE_TYPE_MAPPER = [
+        '1' => '纯文本'
+    ];
+}

+ 5 - 3
composer.json

@@ -10,8 +10,9 @@
         "laravel/framework": "^10.8",
         "laravel/sanctum": "^3.2",
         "laravel/tinker": "^2.8",
-        "predis/predis": "^2.1",
-        "nwidart/laravel-modules": "^10.0"
+        "nwidart/laravel-modules": "^10.0",
+        "overtrue/wechat": "~5.0",
+        "predis/predis": "^2.1"
     },
     "require-dev": {
         "fakerphp/faker": "^1.9.1",
@@ -66,7 +67,8 @@
         "sort-packages": true,
         "allow-plugins": {
             "pestphp/pest-plugin": true,
-            "php-http/discovery": true
+            "php-http/discovery": true,
+            "easywechat-composer/easywechat-composer": true
         }
     },
     "minimum-stability": "stable",

File diff suppressed because it is too large
+ 796 - 77
composer.lock


+ 1 - 1
config/database.php

@@ -182,7 +182,7 @@ return [
             'username' => env('REDIS_USERNAME'),
             'password' => env('REDIS_PASSWORD'),
             'port' => env('REDIS_PORT', '6379'),
-            'database' => env('REDIS_CACHE_DB', '1'),
+            'database' => env('REDIS_CACHE_DB', '0'),
         ],
         //专门处理回传队列的服务器
         'report-redis' => [

+ 23 - 0
config/easywechat.php

@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * easywechat 5.0 公共配置
+ */
+return [
+    'response_type' => 'array',
+    'http' => [
+        'throw'  => true, // 状态码非 200、300 时是否抛出异常,默认为开启
+        'timeout' => 5.0,
+        'retry' => true, // 使用默认重试配置
+    ],
+    'log' => [
+        'default' => 'prod',
+        'channels' => [
+            'prod' => [
+                'driver' => 'daily',
+                'path' => storage_path('logs/easyWechat.log'),
+                'level' => env('EASY_WECHAT_LOG_LEVEL', 'debug'),
+            ]
+        ]
+    ]
+];

+ 1 - 0
config/logging.php

@@ -143,6 +143,7 @@ return [
         'level' => [
             'JuliangAccountReportRanse' => env('LOGGING_JuliangAccountReportRanse', 'info'),
             'reportCharge' => env('LOGGING_reportCharge', 'info'),
+            'KFMessageSend' => env('LOGGING_KFMessageSend', 'debug')
         ],
     ]
  ];

+ 7 - 0
config/wechat.php

@@ -4,5 +4,12 @@ return [
     'duanju' => [
         // 短剧主小程序
         'masterAppid' => env('WECHAT_DUANJU_MASTER_APPID', 'wx86822355ccd03a78'),
+    ],
+    'ug' => [
+        'url' => [
+            'listUser' => env('WECHAT_UG_URL_LIST_USER',
+                'http://m.test.duanju.dududus.com/api/audienceManage/outUserGroup/listUser'),
+        ],
+        'signKey' => 'lV9bOCooPTJ68G3a'
     ]
 ];

+ 23 - 0
tests/Jobs/WechatPlatform/GZHSendKFMessageTest.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace Tests\Jobs\WechatPlatform;
+
+use App\Jobs\WechatPlatform\GZHSendKFMessage;
+use App\Service\Util\Support\Trace\TraceContext;
+use PHPUnit\Framework\TestCase;
+
+class GZHSendKFMessageTest extends \Tests\TestCase
+{
+
+    public function testHandle()
+    {
+        $info = [
+            'gzhId' => 8,
+            'messageId' => 6,
+            'traceInfo' => (new TraceContext())->getTraceInfo()
+        ];
+        $skf = new GZHSendKFMessage($info);
+
+        $skf->handle();
+    }
+}

+ 15 - 0
tests/Service/WechatPlatform/GZHSendKFMessageServiceTest.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace Tests\Service\WechatPlatform;
+
+use App\Service\WechatPlatform\GZHSendKFMessageService;
+use PHPUnit\Framework\TestCase;
+
+class GZHSendKFMessageServiceTest extends \TestCase
+{
+
+    public function testSendText()
+    {
+
+    }
+}