This commit is contained in:
Your Name
2026-08-06 10:57:35 +08:00
parent 2c0b9c5afa
commit 079e50006d
400 changed files with 3046 additions and 714 deletions
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace app\api\controller;
use app\adminapi\logic\qywx\CustomerLogic;
use app\common\service\qywx\QywxCustomerAcquisitionCustomerService;
use EasyWeChat\Kernel\Exceptions\BadRequestException;
use EasyWeChat\Work\Application;
use EasyWeChat\Work\Message;
@@ -17,7 +18,7 @@ use think\facade\Log;
*
* ⚠️ 关于"员工↔客户消息内容"接收:
* 企业微信 **不会** 通过本回调推送客户与员工之间真实的聊天消息内容,
* 这里只处理"添加 / 编辑 / 删除客户"等业务事件(change_external_contact 变体)
* 这里只处理客户关系事件,以及 customer_acquisition 回调中的累计收消息次数;不保存消息正文
* 实时消息接收走「会话内容存档」独立通道:
* - 命令: php think qywx:sync-msg-archive
* - 服务: app\common\service\wechat\QywxMsgArchiveService
@@ -33,15 +34,18 @@ class QywxExternalContactCallbackController extends BaseApiController
{
$corpId = (string) config('pay.wechat_work.corp_id', '');
$customerSecret = (string) config('pay.wechat_work.customer_contact_secret', '');
$acquisitionSecret = (string) config('qywx_customer_acquisition.secret', '');
$payContactSecret = (string) config('pay.wechat_work.external_pay_secret', '');
// 客户联系回调验签需用「接收事件服务器」所属应用的 Secret,优先使用 customer_contact_secret
// 缺省回退到 external_pay_secret 保持向后兼容(同一应用同时具备两类权限的旧部署可继续工作)。
$secret = $customerSecret !== '' ? $customerSecret : $payContactSecret;
$secret = $customerSecret !== ''
? $customerSecret
: ($acquisitionSecret !== '' ? $acquisitionSecret : $payContactSecret);
$token = (string) config('pay.wechat_work.contact_callback_token', '');
$aesKey = (string) config('pay.wechat_work.contact_callback_aes_key', '');
if ($corpId === '' || $secret === '' || $token === '' || $aesKey === '') {
Log::error('qywx external contact callback: 缺少配置 corp_id / customer_contact_secret(或 external_pay_secret) / contact_callback_token / contact_callback_aes_key');
Log::error('qywx external contact callback: 缺少配置 corp_id / customer_contact_secret(或获客助手应用 secret) / contact_callback_token / contact_callback_aes_key');
return response('config error', 503, ['Content-Type' => 'text/plain; charset=utf-8']);
}
@@ -67,6 +71,17 @@ class QywxExternalContactCallbackController extends BaseApiController
return $next($message);
});
$server->addEventListener('customer_acquisition', function (Message $message, \Closure $next) {
try {
(new QywxCustomerAcquisitionCustomerService())->handleCallback($message->toArray());
} catch (\Throwable $e) {
// 服务已将失败事件与 next_retry 落库,定时命令会在 ChatKey 30 分钟内继续重试。
Log::error('qywx customer acquisition callback: ' . $e->getMessage(), ['exception' => $e]);
}
return $next($message);
});
$psr = $server->serve();
$body = $psr->getBody();
$body->rewind();
@@ -4,15 +4,13 @@ declare(strict_types=1);
namespace app\api\controller;
use app\common\service\qywx\QywxPromotionOpenWorkService;
use app\common\service\qywx\QywxPromotionRedirectService;
use think\facade\Log;
/** 企业微信推广公开端点:服务商回调、授权回跳、JS 与随机跳转。 */
/** 企业微信获客助手公开端点:JS 与随机跳转。 */
class QywxPromotionPublicController extends BaseApiController
{
/** 公开安装代码随机跳转与企业微信服务商回调均不依赖前台用户登录。 */
public array $notNeedLogin = ['script', 'redirect', 'providerCallback', 'authCallback'];
/** 公开安装代码随机跳转不依赖前台用户登录。 */
public array $notNeedLogin = ['script', 'redirect'];
public function script(string $key)
{
@@ -58,7 +56,7 @@ JS;
'ip' => (string) $this->request->ip(),
]);
if (!$picked) {
return response('当前暂无可用的企业微信推广链接,请稍后再试。', 503, [
return response('当前暂无可用的企业微信获客助手链接,请稍后再试。', 503, [
'Content-Type' => 'text/plain; charset=utf-8',
'Cache-Control' => 'no-store',
]);
@@ -70,40 +68,4 @@ JS;
]);
}
public function providerCallback()
{
try {
$psr = QywxPromotionOpenWorkService::serveProviderCallback();
$body = $psr->getBody();
$body->rewind();
$headers = [];
if ($psr->getHeaderLine('Content-Type') !== '') {
$headers['Content-Type'] = $psr->getHeaderLine('Content-Type');
}
return response($body->getContents(), $psr->getStatusCode(), $headers);
} catch (\Throwable $e) {
Log::error('企微推广服务商回调失败:' . $e->getMessage(), ['exception' => $e]);
return response('error', 500, ['Content-Type' => 'text/plain; charset=utf-8']);
}
}
public function authCallback()
{
$fallback = rtrim($this->request->domain(), '/') . '/admin/first_visit/wecom_promotion';
$returnUrl = QywxPromotionOpenWorkService::configuredAdminReturnUrl($fallback);
try {
$result = QywxPromotionOpenWorkService::consumeAuthorizationCallback(
trim((string) $this->request->get('auth_code', '')),
trim((string) $this->request->get('state', ''))
);
$query = ['wecom_auth' => 'success', 'account_id' => (int) $result['id']];
} catch (\Throwable $e) {
Log::error('企微推广授权回跳失败:' . $e->getMessage(), ['exception' => $e]);
$query = ['wecom_auth' => 'failed', 'message' => mb_substr($e->getMessage(), 0, 160)];
}
return redirect($returnUrl . (str_contains($returnUrl, '?') ? '&' : '?') . http_build_query($query), 302);
}
}
+1 -3
View File
@@ -11,8 +11,6 @@ use think\facade\Route;
Route::rule('qywx/external-contact/notify', 'QywxExternalContactCallback/notify', 'GET|POST');
Route::post('ej-pharmacy/webhook', 'EjPharmacyCallback/webhook');
// 企业微信推广助手:服务商应用指令、授权回跳、公开 JS 与随机分流。
Route::rule('qywx-promotion/provider/callback', 'QywxPromotionPublic/providerCallback', 'GET|POST');
Route::get('qywx-promotion/auth/callback', 'QywxPromotionPublic/authCallback');
// 企业微信内部应用推广助手:公开 JS 与服务端随机分流。
Route::get('qywx-promotion/js/:key', 'QywxPromotionPublic/script');
Route::get('qywx-promotion/go/:key', 'QywxPromotionPublic/redirect');