更新
This commit is contained in:
@@ -445,6 +445,8 @@ class WecomPromotionLogic
|
||||
),
|
||||
'member_userids' => $userIds,
|
||||
'range_userids' => $eligibleUserIds,
|
||||
// 前端据此确认标签、欢迎语等扩展配置已和方案一并提交并完成回读校验。
|
||||
'automation_saved' => $automation !== null,
|
||||
'sync_error' => $syncError,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -130,10 +130,11 @@ class QywxExternalContactCallbackController extends BaseApiController
|
||||
$failReason = (string) ($message['FailReason'] ?? '');
|
||||
$eventTime = (int) ($message['CreateTime'] ?? 0);
|
||||
|
||||
// 只接管已保存新配置且可核验方案/成员的推广事件;这里无任何远端请求。
|
||||
// 只接管已保存新配置且可核验方案/成员的推广事件。
|
||||
// 回调内立即尝试欢迎语和正式客户标签,常驻/分钟任务继续承担重试及其余慢动作。
|
||||
// 同时处理带欢迎码的半客户,避免原来的半客户早返回吞掉20秒欢迎语窗口。
|
||||
$event = $message instanceof Message ? $message->toArray() : (array) $message;
|
||||
$queued = (new QywxPromotionAutomationService())->enqueueVerifiedEvent($event);
|
||||
$queued = (new QywxPromotionAutomationService())->enqueueVerifiedEvent($event, true);
|
||||
$auditEvent = $event;
|
||||
unset($auditEvent['WelcomeCode']);
|
||||
|
||||
@@ -151,7 +152,7 @@ class QywxExternalContactCallbackController extends BaseApiController
|
||||
]);
|
||||
|
||||
if ($queued) {
|
||||
// 常驻worker先发欢迎语,分钟补偿完成标签/备注、成员记账、范围与客户资料同步。
|
||||
// 即时通道已尝试欢迎语/标签;分钟补偿完成备注、成员记账、范围与客户资料同步。
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ class QywxPromotionAutomationService
|
||||
|
||||
/**
|
||||
* 仅供验签解密后的回调调用。false代表沿用旧同步流程;已接管的入队错误必须返回HTTP500。
|
||||
* 此处无网络请求,保证回调不等待客户详情、范围更新或素材上传。
|
||||
* 默认仅入队;回调入口可启用即时通道,在当前请求内先发欢迎语并给正式客户打标。
|
||||
*/
|
||||
public function enqueueVerifiedEvent(array $event): bool
|
||||
public function enqueueVerifiedEvent(array $event, bool $processImmediately = false): bool
|
||||
{
|
||||
$change = (string) ($event['ChangeType'] ?? '');
|
||||
if (!in_array($change, ['add_external_contact', 'add_half_external_contact'], true)) {
|
||||
@@ -85,7 +85,7 @@ class QywxPromotionAutomationService
|
||||
'sync' => self::action($half ? 'skipped' : 'pending', $half ? 'half_contact' : ''),
|
||||
];
|
||||
$corp = (string) ($event['ToUserName'] ?? config('pay.wechat_work.corp_id', ''));
|
||||
$this->store->enqueue([
|
||||
$taskId = $this->store->enqueue([
|
||||
'event_key' => hash('sha256', implode('|', [$corp, $change, $userid, $external, (string) $eventTime])),
|
||||
'pool_id' => $attribution['pool_id'], 'member_admin_id' => $attribution['member_admin_id'],
|
||||
'change_type' => $change, 'userid' => $userid, 'external_userid' => $external,
|
||||
@@ -98,6 +98,14 @@ class QywxPromotionAutomationService
|
||||
'status' => self::allTerminal($actions) ? 'done' : 'pending', 'next_retry' => 0,
|
||||
'lock_token' => '', 'lock_until' => 0, 'create_time' => $now, 'update_time' => $now,
|
||||
]);
|
||||
if ($processImmediately) {
|
||||
// 部署环境暂未启动常驻 worker 时仍要抢住 20 秒欢迎码窗口。
|
||||
// 标签只在正式客户事件执行;其余慢动作仍由分钟补偿处理。
|
||||
$this->consumeIds('welcome', [$taskId]);
|
||||
if (!$half) {
|
||||
$this->consumeIds('inline_metadata', [$taskId], ['tags']);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (\Throwable) {
|
||||
// 不附原异常,入库SQL可能包含密文和配置;回调层返回500触发企微重试。
|
||||
@@ -130,9 +138,15 @@ class QywxPromotionAutomationService
|
||||
}
|
||||
|
||||
private function consume(string $lane, int $limit): array
|
||||
{
|
||||
return $this->consumeIds($lane, $this->store->due($lane, $this->now(), $limit));
|
||||
}
|
||||
|
||||
/** @param list<int> $ids @param list<string>|null $metadataActions */
|
||||
private function consumeIds(string $lane, array $ids, ?array $metadataActions = null): array
|
||||
{
|
||||
$result = ['selected' => 0, 'processed' => 0, 'failed' => 0];
|
||||
foreach ($this->store->due($lane, $this->now(), $limit) as $id) {
|
||||
foreach ($ids as $id) {
|
||||
++$result['selected'];
|
||||
try {
|
||||
$row = $this->store->claim($id, $lane, $this->now());
|
||||
@@ -141,7 +155,8 @@ class QywxPromotionAutomationService
|
||||
}
|
||||
$actions = json_decode($row['actions_json'], true, 512, JSON_THROW_ON_ERROR);
|
||||
$config = json_decode($row['config_json'], true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!self::terminal($actions['welcome']['status'])) {
|
||||
// 即时标签与欢迎语共用同一任务,但不能把仍在 20 秒窗口内待重试的欢迎语判为过期。
|
||||
if ($lane !== 'inline_metadata' && !self::terminal($actions['welcome']['status'])) {
|
||||
if ($lane === 'welcome') {
|
||||
$this->welcome($row, $actions, $config);
|
||||
} else {
|
||||
@@ -151,7 +166,7 @@ class QywxPromotionAutomationService
|
||||
}
|
||||
}
|
||||
if ($lane !== 'welcome') {
|
||||
$this->metadata($row, $actions, $config);
|
||||
$this->metadata($row, $actions, $config, $metadataActions);
|
||||
}
|
||||
$row['lock_until'] = 0;
|
||||
$row['update_time'] = $this->now();
|
||||
@@ -225,10 +240,15 @@ class QywxPromotionAutomationService
|
||||
$this->transition($row, $actions, 'welcome', $expired ? 'expired' : 'retry', $reason, $code, $this->now() + 1);
|
||||
}
|
||||
|
||||
private function metadata(array &$row, array &$actions, array $config): void
|
||||
/** @param list<string>|null $only */
|
||||
private function metadata(array &$row, array &$actions, array $config, ?array $only = null): void
|
||||
{
|
||||
$names = null;
|
||||
foreach (['tags', 'remark', 'description', 'dispatch', 'range', 'sync'] as $name) {
|
||||
$namesToProcess = ['tags', 'remark', 'description', 'dispatch', 'range', 'sync'];
|
||||
if ($only !== null) {
|
||||
$namesToProcess = array_values(array_intersect($namesToProcess, $only));
|
||||
}
|
||||
foreach ($namesToProcess as $name) {
|
||||
if (self::terminal($actions[$name]['status']) || (int) ($actions[$name]['next_retry'] ?? 0) > $this->now()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -109,11 +109,12 @@ class QywxPromotionAutomationStore
|
||||
}
|
||||
$pendingWelcome = in_array($row['welcome_status'], ['pending', 'retry', 'running'], true);
|
||||
if (($lane === 'welcome' && (!$pendingWelcome || (int) $row['welcome_next_retry'] > $now))
|
||||
|| ($lane !== 'welcome' && (($pendingWelcome && (int) $row['welcome_expires_at'] > $now) || (int) $row['next_retry'] > $now))) {
|
||||
|| ($lane === 'metadata' && (($pendingWelcome && (int) $row['welcome_expires_at'] > $now) || (int) $row['next_retry'] > $now))
|
||||
|| ($lane === 'inline_metadata' && (int) $row['next_retry'] > $now)) {
|
||||
return null;
|
||||
}
|
||||
$row['lock_token'] = bin2hex(random_bytes(16));
|
||||
$row['lock_until'] = $now + ($lane === 'welcome' ? 30 : 600);
|
||||
$row['lock_until'] = $now + (in_array($lane, ['welcome', 'inline_metadata'], true) ? 30 : 600);
|
||||
Db::name('qywx_promotion_automation_task')->where('id', $id)->update([
|
||||
'lock_token' => $row['lock_token'], 'lock_until' => $row['lock_until'], 'update_time' => $now,
|
||||
]);
|
||||
|
||||
@@ -54,7 +54,7 @@ class QywxPromotionCodeCipher
|
||||
return $this->key();
|
||||
}
|
||||
$directory = root_path('runtime') . 'qywx_promotion_private';
|
||||
if (!is_dir($directory) && !mkdir($directory, 0700, true) && !is_dir($directory)) {
|
||||
if (!is_dir($directory) && !@mkdir($directory, 0700, true) && !is_dir($directory)) {
|
||||
throw new RuntimeException('无法创建欢迎码私有密钥目录');
|
||||
}
|
||||
$path = $directory . DIRECTORY_SEPARATOR . 'welcome.key';
|
||||
|
||||
@@ -28,7 +28,17 @@ class QywxPromotionConfig
|
||||
public static function installed(): bool
|
||||
{
|
||||
try {
|
||||
return Db::name('qywx_promotion_config')->getFields() !== [];
|
||||
foreach ([
|
||||
'qywx_promotion_config',
|
||||
'qywx_promotion_media',
|
||||
'qywx_promotion_automation_task',
|
||||
'qywx_promotion_automation_action_log',
|
||||
] as $table) {
|
||||
if (Db::name($table)->getFields() === []) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (\Throwable $error) {
|
||||
// 仅旧部署未建表时回退。数据库故障不能退回全天路由、忽略排班配置。
|
||||
if (str_contains($error->getMessage(), '42S02')
|
||||
@@ -64,13 +74,21 @@ class QywxPromotionConfig
|
||||
public static function save(int $poolId, array $config): void
|
||||
{
|
||||
self::assertInstalled();
|
||||
if ($poolId <= 0) {
|
||||
throw new RuntimeException('分流方案不存在,无法保存自动化配置');
|
||||
}
|
||||
$row = Db::name('qywx_promotion_config')->where('pool_id', $poolId)->find();
|
||||
$data = ['config_json' => json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR), 'update_time' => time()];
|
||||
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
$data = ['config_json' => $json, 'update_time' => time()];
|
||||
if ($row) {
|
||||
Db::name('qywx_promotion_config')->where('pool_id', $poolId)->update($data);
|
||||
} else {
|
||||
Db::name('qywx_promotion_config')->insert($data + ['pool_id' => $poolId, 'create_time' => time()]);
|
||||
}
|
||||
$saved = Db::name('qywx_promotion_config')->where('pool_id', $poolId)->value('config_json');
|
||||
if (!is_string($saved) || !hash_equals($json, $saved)) {
|
||||
throw new RuntimeException('获客标签与欢迎语配置未能完整落库,请重试');
|
||||
}
|
||||
}
|
||||
|
||||
/** 不接受浏览器提供的 userid;成员归属必须经过现有后台数据权限校验后再绑定。 */
|
||||
|
||||
Reference in New Issue
Block a user