更新
This commit is contained in:
@@ -6,12 +6,15 @@ namespace app\common\service\qywx;
|
||||
|
||||
use app\common\model\QywxExternalContact;
|
||||
use app\common\model\QywxMediaChannel;
|
||||
use think\facade\Cache;
|
||||
use think\facade\Db;
|
||||
use think\db\Query;
|
||||
|
||||
class MediaChannelService
|
||||
{
|
||||
private const ACTIVE_ROWS_CACHE_TTL_SECONDS = 1.0;
|
||||
private const CURRENT_TAG_CATALOG_CACHE_KEY = 'qywx:current_tag_catalog:v1';
|
||||
private const CURRENT_TAG_CATALOG_CACHE_TTL_SECONDS = 30;
|
||||
private const SCAN_DUPLICATE_UPDATE_FIELDS = [
|
||||
'source_group_name',
|
||||
'last_seen_time',
|
||||
@@ -23,6 +26,16 @@ class MediaChannelService
|
||||
|
||||
private static float $activeChannelRowsCachedAt = 0.0;
|
||||
|
||||
/** @var array<int, array{tag_id: string, tag_name: string, group_name: string, customer_count: int}>|null */
|
||||
private static ?array $currentTagCatalogCache = null;
|
||||
|
||||
private static float $currentTagCatalogCachedAt = 0.0;
|
||||
|
||||
/** @var array<int, array<string, mixed>>|null */
|
||||
private static ?array $currentTagChannelRowsCache = null;
|
||||
|
||||
private static float $currentTagChannelRowsCachedAt = 0.0;
|
||||
|
||||
/**
|
||||
* 与业绩看板「渠道来源」相同的分组(按 source_group_name),不含客户数统计。
|
||||
*
|
||||
@@ -86,6 +99,133 @@ class MediaChannelService
|
||||
], $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* 企微客户页与一诊渠道共用的当前标签目录。
|
||||
*
|
||||
* 只统计仍关联未删除客户的标签;同一个 tag_id 只保留更新时间最新、
|
||||
* 同时间 id 最大的一份名称和分组,避免标签改名后同时展示新旧快照。
|
||||
*
|
||||
* @return array<int, array{tag_id: string, tag_name: string, group_name: string, customer_count: int}>
|
||||
*/
|
||||
public static function getCurrentTagCatalog(): array
|
||||
{
|
||||
$now = microtime(true);
|
||||
if (self::$currentTagCatalogCache !== null
|
||||
&& ($now - self::$currentTagCatalogCachedAt) < self::CURRENT_TAG_CATALOG_CACHE_TTL_SECONDS) {
|
||||
return self::$currentTagCatalogCache;
|
||||
}
|
||||
|
||||
try {
|
||||
$cachedCatalog = Cache::get(self::CURRENT_TAG_CATALOG_CACHE_KEY);
|
||||
} catch (\Throwable) {
|
||||
// 缓存目录/服务不可用时直接查库,缓存不能阻断业务接口。
|
||||
$cachedCatalog = null;
|
||||
}
|
||||
if (is_array($cachedCatalog)) {
|
||||
self::$currentTagCatalogCache = $cachedCatalog;
|
||||
self::$currentTagCatalogCachedAt = microtime(true);
|
||||
|
||||
return self::$currentTagCatalogCache;
|
||||
}
|
||||
|
||||
$tagTable = self::tableWithPrefix('qywx_external_contact_tag');
|
||||
$contactTable = self::tableWithPrefix('qywx_external_contact');
|
||||
$sql = <<<SQL
|
||||
SELECT latest_tag.tag_id,
|
||||
COALESCE(latest_tag.tag_name, '') AS tag_name,
|
||||
COALESCE(latest_tag.group_name, '') AS group_name,
|
||||
current_tag.customer_count
|
||||
FROM {$tagTable} latest_tag
|
||||
INNER JOIN (
|
||||
SELECT tagged.tag_id,
|
||||
MAX(CONCAT(LPAD(tagged.update_time, 10, '0'), LPAD(tagged.id, 10, '0'))) AS latest_sort_key,
|
||||
COUNT(DISTINCT tagged.external_userid) AS customer_count
|
||||
FROM {$tagTable} tagged
|
||||
INNER JOIN {$contactTable} active_contact
|
||||
ON active_contact.external_userid = tagged.external_userid
|
||||
AND active_contact.delete_time IS NULL
|
||||
WHERE tagged.tag_id <> ''
|
||||
GROUP BY tagged.tag_id
|
||||
) current_tag
|
||||
ON current_tag.tag_id = latest_tag.tag_id
|
||||
AND current_tag.latest_sort_key = CONCAT(
|
||||
LPAD(latest_tag.update_time, 10, '0'),
|
||||
LPAD(latest_tag.id, 10, '0')
|
||||
)
|
||||
ORDER BY current_tag.customer_count DESC, latest_tag.tag_id ASC
|
||||
SQL;
|
||||
|
||||
self::$currentTagCatalogCache = array_map(static fn (array $row): array => [
|
||||
'tag_id' => trim((string) ($row['tag_id'] ?? '')),
|
||||
'tag_name' => trim((string) ($row['tag_name'] ?? '')),
|
||||
'group_name' => trim((string) ($row['group_name'] ?? '')),
|
||||
'customer_count' => (int) ($row['customer_count'] ?? 0),
|
||||
], Db::query($sql));
|
||||
self::$currentTagCatalogCachedAt = microtime(true);
|
||||
try {
|
||||
Cache::set(
|
||||
self::CURRENT_TAG_CATALOG_CACHE_KEY,
|
||||
self::$currentTagCatalogCache,
|
||||
self::CURRENT_TAG_CATALOG_CACHE_TTL_SECONDS
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
// 同上:共享缓存仅用于加速,当前请求的内存缓存仍然有效。
|
||||
}
|
||||
|
||||
return self::$currentTagCatalogCache;
|
||||
}
|
||||
|
||||
public static function forgetCurrentTagCatalogCache(): void
|
||||
{
|
||||
self::$currentTagCatalogCache = null;
|
||||
self::$currentTagCatalogCachedAt = 0.0;
|
||||
self::$currentTagChannelRowsCache = null;
|
||||
self::$currentTagChannelRowsCachedAt = 0.0;
|
||||
try {
|
||||
Cache::delete(self::CURRENT_TAG_CATALOG_CACHE_KEY);
|
||||
} catch (\Throwable) {
|
||||
// 缓存不可用不影响标签同步和后续数据库读取。
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 一诊专用渠道选项:严格投影当前企微标签,不混入历史 name-only 渠道。
|
||||
*
|
||||
* @return array<int, array{code: string, name: string, tag_id: string, group_name: string, customer_count: int}>
|
||||
*/
|
||||
public static function getCurrentTagOptions(): array
|
||||
{
|
||||
return array_map(static fn (array $row): array => [
|
||||
'code' => (string) ($row['channel_code'] ?? ''),
|
||||
'name' => (string) ($row['channel_name'] ?? ''),
|
||||
'tag_id' => (string) ($row['source_tag_id'] ?? ''),
|
||||
'group_name' => (string) ($row['source_group_name'] ?? ''),
|
||||
'customer_count' => (int) ($row['customer_count'] ?? 0),
|
||||
], self::getCurrentTagChannelRows());
|
||||
}
|
||||
|
||||
/**
|
||||
* 一诊专用解析器。当前标签即使在历史渠道注册表中被停用,也仍按企微当前标签生效;
|
||||
* 全局财务渠道的启停语义继续由 getChannelByCode() 维护。
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public static function getCurrentTagChannelByCode(string $channelCode): ?array
|
||||
{
|
||||
$channelCode = trim($channelCode);
|
||||
if ($channelCode === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (self::getCurrentTagChannelRows() as $row) {
|
||||
if ((string) ($row['channel_code'] ?? '') === $channelCode) {
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function getDefaultCode(): string
|
||||
{
|
||||
$rows = self::getActiveChannelRows();
|
||||
@@ -225,8 +365,14 @@ class MediaChannelService
|
||||
$tagId = trim((string) ($channel['source_tag_id'] ?? ''));
|
||||
if ($tagId !== '') {
|
||||
$tagTable = self::tableWithPrefix('qywx_external_contact_tag');
|
||||
$contactTable = self::tableWithPrefix('qywx_external_contact');
|
||||
$query->whereRaw(
|
||||
"{$field} IN (SELECT channel_tag.external_userid FROM {$tagTable} channel_tag WHERE channel_tag.tag_id = ?)",
|
||||
"{$field} IN ("
|
||||
. "SELECT channel_tag.external_userid FROM {$tagTable} channel_tag "
|
||||
. 'WHERE channel_tag.tag_id = ? '
|
||||
. "AND EXISTS (SELECT 1 FROM {$contactTable} active_channel_contact "
|
||||
. 'WHERE active_channel_contact.external_userid = channel_tag.external_userid '
|
||||
. 'AND active_channel_contact.delete_time IS NULL))',
|
||||
[$tagId]
|
||||
);
|
||||
|
||||
@@ -321,6 +467,8 @@ class MediaChannelService
|
||||
|
||||
self::$activeChannelRowsCache = null;
|
||||
self::$activeChannelRowsCachedAt = 0.0;
|
||||
self::$currentTagChannelRowsCache = null;
|
||||
self::$currentTagChannelRowsCachedAt = 0.0;
|
||||
|
||||
return [
|
||||
'scanned_contacts' => $scannedContacts,
|
||||
@@ -329,6 +477,105 @@ class MediaChannelService
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function getCurrentTagChannelRows(): array
|
||||
{
|
||||
$now = microtime(true);
|
||||
if (self::$currentTagChannelRowsCache !== null
|
||||
&& ($now - self::$currentTagChannelRowsCachedAt) < self::CURRENT_TAG_CATALOG_CACHE_TTL_SECONDS) {
|
||||
return self::$currentTagChannelRowsCache;
|
||||
}
|
||||
|
||||
$catalog = self::getCurrentTagCatalog();
|
||||
$tagIds = array_values(array_filter(array_map(
|
||||
static fn (array $tag): string => trim((string) ($tag['tag_id'] ?? '')),
|
||||
$catalog
|
||||
), static fn (string $tagId): bool => $tagId !== ''));
|
||||
|
||||
$configuredRows = [];
|
||||
if ($tagIds !== []) {
|
||||
$configuredRows = QywxMediaChannel::whereIn('source_tag_id', $tagIds)
|
||||
->field(
|
||||
'id, channel_code, channel_name, source_tag_id, source_tag_name, source_group_name, '
|
||||
. 'tag_uniq_key, status, last_seen_time, create_time, update_time'
|
||||
)
|
||||
->order('id asc')
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
self::$currentTagChannelRowsCache = self::mergeCurrentTagsWithConfiguredChannels($catalog, $configuredRows);
|
||||
self::$currentTagChannelRowsCachedAt = microtime(true);
|
||||
|
||||
return self::$currentTagChannelRowsCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前企微标签决定展示名称和可见集合;注册表只提供稳定 code 及历史名称兼容。
|
||||
* 历史 name-only 行不会进入结果,注册表 status 也不会隐藏仍在使用的企微标签。
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $catalog
|
||||
* @param array<int, array<string, mixed>> $configuredRows
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function mergeCurrentTagsWithConfiguredChannels(array $catalog, array $configuredRows): array
|
||||
{
|
||||
$configuredByTagId = [];
|
||||
foreach ($configuredRows as $configuredRow) {
|
||||
$tagId = trim((string) ($configuredRow['source_tag_id'] ?? ''));
|
||||
if ($tagId !== '' && !isset($configuredByTagId[$tagId])) {
|
||||
$configuredByTagId[$tagId] = $configuredRow;
|
||||
}
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($catalog as $tag) {
|
||||
$tagId = trim((string) ($tag['tag_id'] ?? $tag['source_tag_id'] ?? ''));
|
||||
if ($tagId === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tagName = trim((string) ($tag['tag_name'] ?? $tag['source_tag_name'] ?? ''));
|
||||
$groupName = trim((string) ($tag['group_name'] ?? $tag['source_group_name'] ?? ''));
|
||||
$configured = $configuredByTagId[$tagId] ?? [];
|
||||
$channelCode = trim((string) ($configured['channel_code'] ?? ''));
|
||||
if ($channelCode === '') {
|
||||
$channelCode = self::buildChannelCode($tagId, $tagName);
|
||||
}
|
||||
if ($channelCode === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$row = $configured;
|
||||
$oldChannelName = trim((string) ($configured['channel_name'] ?? ''));
|
||||
$oldTagName = trim((string) ($configured['source_tag_name'] ?? ''));
|
||||
if ($oldChannelName !== '' && $oldChannelName !== $tagName) {
|
||||
$row['legacy_channel_name'] = $oldChannelName;
|
||||
}
|
||||
if ($oldTagName !== '' && $oldTagName !== $tagName) {
|
||||
$row['legacy_source_tag_name'] = $oldTagName;
|
||||
}
|
||||
|
||||
$row['id'] = (int) ($configured['id'] ?? 0);
|
||||
$row['channel_code'] = $channelCode;
|
||||
$row['channel_name'] = $tagName !== '' ? $tagName : $tagId;
|
||||
$row['source_tag_id'] = $tagId;
|
||||
$row['source_tag_name'] = $tagName;
|
||||
$row['source_group_name'] = $groupName;
|
||||
$row['tag_uniq_key'] = self::buildTagUniqKey($tagId, $tagName);
|
||||
$row['status'] = 1;
|
||||
$row['customer_count'] = (int) ($tag['customer_count'] ?? 0);
|
||||
$row['last_seen_time'] = (int) ($configured['last_seen_time'] ?? 0);
|
||||
$row['create_time'] = (int) ($configured['create_time'] ?? 0);
|
||||
$row['update_time'] = (int) ($configured['update_time'] ?? 0);
|
||||
$rows[] = $row;
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine the persistent channel registry with the latest tag snapshots in
|
||||
* the normalized relation table. The registry keeps stable channel codes
|
||||
|
||||
Reference in New Issue
Block a user