This commit is contained in:
Your Name
2026-09-07 12:30:42 +08:00
parent d5164b7369
commit 9ef6eb8d67
369 changed files with 11733 additions and 0 deletions
@@ -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,
]);
}
}