更新
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\qywx;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 企业微信新增事件的标签快照。
|
||||
*
|
||||
* qywx_external_contact_tag 是当前状态投影,客户改标签或删除后会被覆盖/清理;
|
||||
* 本服务只在 add_external_contact 发生时写入,之后永不更新或删除。
|
||||
*/
|
||||
class QywxExternalContactEventTagSnapshotService
|
||||
{
|
||||
public const SOURCE_CONTACT_DETAIL = 1;
|
||||
public const SOURCE_PROMOTION_TASK = 2;
|
||||
|
||||
private static ?bool $installed = null;
|
||||
|
||||
/**
|
||||
* 灰度发布保护:代码先于迁移生效时,读取侧可以显式降级而不是返回 500。
|
||||
*/
|
||||
public static function installed(): bool
|
||||
{
|
||||
if (self::$installed !== null) {
|
||||
return self::$installed;
|
||||
}
|
||||
|
||||
try {
|
||||
self::$installed = Db::name('qywx_external_contact_event_tag')->getFields() !== [];
|
||||
|
||||
return self::$installed;
|
||||
} catch (\Throwable $e) {
|
||||
$message = $e->getMessage();
|
||||
if (str_contains($message, '42S02')
|
||||
|| str_contains($message, '1146')
|
||||
|| str_contains($message, 'no such table')) {
|
||||
self::$installed = false;
|
||||
|
||||
return self::$installed;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 /externalcontact/get 的 follow_user[] 中,只截取产生事件的员工标签。
|
||||
* 找不到该员工时不写空快照,避免把一次不完整同步误判为“当时无标签”。
|
||||
*
|
||||
* @param array<int, mixed> $followUsers
|
||||
*/
|
||||
public static function captureFromFollowUsers(int $eventId, string $followUserId, array $followUsers): void
|
||||
{
|
||||
$followUserId = trim($followUserId);
|
||||
if ($eventId <= 0 || $followUserId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($followUsers as $followUser) {
|
||||
if (!is_array($followUser)
|
||||
|| trim((string) ($followUser['userid'] ?? '')) !== $followUserId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
self::capture(
|
||||
$eventId,
|
||||
$followUserId,
|
||||
is_array($followUser['tags'] ?? null) ? $followUser['tags'] : [],
|
||||
self::SOURCE_CONTACT_DETAIL
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回调入库后,用同一推广任务的不可变 config_json 补写快照。
|
||||
*/
|
||||
public static function captureForPromotionEvent(int $eventId): void
|
||||
{
|
||||
if ($eventId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$event = Db::name('qywx_external_contact_event')->where('id', $eventId)->find();
|
||||
if (!$event || (string) ($event['change_type'] ?? '') !== 'add_external_contact') {
|
||||
return;
|
||||
}
|
||||
|
||||
$task = Db::name('qywx_promotion_automation_task')
|
||||
->where('change_type', (string) $event['change_type'])
|
||||
->where('userid', (string) $event['user_id'])
|
||||
->where('external_userid', (string) $event['external_userid'])
|
||||
->where('event_time', (int) $event['event_time'])
|
||||
->find();
|
||||
if ($task) {
|
||||
self::captureFromPromotionTask($task, $eventId);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
self::logFailure($e, $eventId, 'promotion_event');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签动作成功后追加推广配置中的确定标签,但不写完成标记。
|
||||
* 推广配置只描述自动添加的标签,不能证明客户当时没有其他标签;完整快照由后续客户详情同步完成。
|
||||
*
|
||||
* @param array<string, mixed> $task
|
||||
*/
|
||||
public static function captureFromPromotionTask(array $task, int $knownEventId = 0): void
|
||||
{
|
||||
if ((string) ($task['change_type'] ?? '') !== 'add_external_contact') {
|
||||
return;
|
||||
}
|
||||
|
||||
$actions = json_decode((string) ($task['actions_json'] ?? ''), true);
|
||||
$tagStatus = is_array($actions)
|
||||
? (string) ($actions['tags']['status'] ?? '')
|
||||
: '';
|
||||
if ($tagStatus !== 'success') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$eventId = $knownEventId;
|
||||
if ($eventId <= 0) {
|
||||
$eventId = (int) Db::name('qywx_external_contact_event')
|
||||
->where('change_type', 'add_external_contact')
|
||||
->where('user_id', (string) ($task['userid'] ?? ''))
|
||||
->where('external_userid', (string) ($task['external_userid'] ?? ''))
|
||||
->where('event_time', (int) ($task['event_time'] ?? 0))
|
||||
->value('id');
|
||||
}
|
||||
if ($eventId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tags = [];
|
||||
$config = json_decode((string) ($task['config_json'] ?? ''), true);
|
||||
foreach ((array) ($config['tag_ids'] ?? []) as $tagId) {
|
||||
$tagId = trim((string) $tagId);
|
||||
if ($tagId !== '') {
|
||||
$tags[] = ['tag_id' => $tagId];
|
||||
}
|
||||
}
|
||||
if ($tags === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::appendTags(
|
||||
$eventId,
|
||||
(string) ($task['userid'] ?? ''),
|
||||
$tags,
|
||||
self::SOURCE_PROMOTION_TASK
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
self::logFailure($e, $knownEventId, 'promotion_task');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加推广任务能够证明的标签,不写 tag_id='' 完成标记。
|
||||
*
|
||||
* @param array<int, mixed> $tags
|
||||
*/
|
||||
private static function appendTags(int $eventId, string $followUserId, array $tags, int $source): void
|
||||
{
|
||||
$followUserId = mb_substr(trim($followUserId), 0, 64);
|
||||
if ($eventId <= 0 || $followUserId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
foreach (self::normalizeTags($tags) as $tag) {
|
||||
self::insertIgnore([
|
||||
'event_id' => $eventId,
|
||||
'follow_user_id' => $followUserId,
|
||||
'tag_id' => $tag['tag_id'],
|
||||
'tag_name' => $tag['tag_name'],
|
||||
'group_name' => $tag['group_name'],
|
||||
'snapshot_source' => $source,
|
||||
'create_time' => time(),
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
self::logFailure($e, $eventId, 'append_tags');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 先写 tag_id='' 完成标记,再写真实标签;同一事务保证不会留下半份快照。
|
||||
* 完成标记已存在时直接返回,使重复回调不会把后来新增的标签补进历史事件。
|
||||
*
|
||||
* @param array<int, mixed> $tags
|
||||
*/
|
||||
private static function capture(int $eventId, string $followUserId, array $tags, int $source): void
|
||||
{
|
||||
$followUserId = mb_substr(trim($followUserId), 0, 64);
|
||||
if ($eventId <= 0 || $followUserId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$normalized = self::normalizeTags($tags);
|
||||
|
||||
try {
|
||||
Db::transaction(static function () use ($eventId, $followUserId, $normalized, $source): void {
|
||||
$inserted = self::insertIgnore([
|
||||
'event_id' => $eventId,
|
||||
'follow_user_id' => $followUserId,
|
||||
'tag_id' => '',
|
||||
'tag_name' => '',
|
||||
'group_name' => '',
|
||||
'snapshot_source' => $source,
|
||||
'create_time' => time(),
|
||||
]);
|
||||
if ($inserted === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($normalized as $tag) {
|
||||
self::insertIgnore([
|
||||
'event_id' => $eventId,
|
||||
'follow_user_id' => $followUserId,
|
||||
'tag_id' => $tag['tag_id'],
|
||||
'tag_name' => $tag['tag_name'],
|
||||
'group_name' => $tag['group_name'],
|
||||
'snapshot_source' => $source,
|
||||
'create_time' => time(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
// 快照是统计增强,迁移未执行或短时 DB 异常不能阻断企微回调主链路。
|
||||
self::logFailure($e, $eventId, 'capture');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $tags
|
||||
* @return array<string, array{tag_id:string,tag_name:string,group_name:string}>
|
||||
*/
|
||||
private static function normalizeTags(array $tags): array
|
||||
{
|
||||
$normalized = [];
|
||||
foreach ($tags as $tag) {
|
||||
if (!is_array($tag)) {
|
||||
continue;
|
||||
}
|
||||
$tagId = mb_substr(trim((string) ($tag['tag_id'] ?? $tag['id'] ?? '')), 0, 64);
|
||||
if ($tagId === '') {
|
||||
continue;
|
||||
}
|
||||
$normalized[$tagId] = [
|
||||
'tag_id' => $tagId,
|
||||
'tag_name' => mb_substr((string) ($tag['tag_name'] ?? $tag['name'] ?? ''), 0, 128),
|
||||
'group_name' => mb_substr((string) ($tag['group_name'] ?? ''), 0, 128),
|
||||
];
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/** @param array<string, int|string> $row */
|
||||
private static function insertIgnore(array $row): int
|
||||
{
|
||||
$table = (string) config('database.connections.mysql.prefix')
|
||||
. 'qywx_external_contact_event_tag';
|
||||
$columns = array_keys($row);
|
||||
$sql = 'INSERT IGNORE INTO `' . $table . '` (`' . implode('`,`', $columns) . '`) VALUES ('
|
||||
. implode(',', array_fill(0, count($columns), '?')) . ')';
|
||||
|
||||
return Db::execute($sql, array_values($row));
|
||||
}
|
||||
|
||||
private static function logFailure(\Throwable $e, int $eventId, string $stage): void
|
||||
{
|
||||
Log::warning('qywx external contact event tag snapshot failed: ' . $e->getMessage(), [
|
||||
'event_id' => $eventId,
|
||||
'stage' => $stage,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user