更新
This commit is contained in:
@@ -189,6 +189,7 @@ class AppointmentLogic extends BaseLogic
|
||||
$slots[] = [
|
||||
'time' => $time,
|
||||
'available' => true,
|
||||
'has_appointment' => false,
|
||||
'quota' => 1,
|
||||
'period' => $roster['period'] ?? 'segment',
|
||||
];
|
||||
@@ -224,28 +225,39 @@ class AppointmentLogic extends BaseLogic
|
||||
'last_3_slots' => array_slice($slots, -3),
|
||||
]);
|
||||
|
||||
// 4. 查询已预约的时间段
|
||||
$appointmentTimes = Appointment::where([
|
||||
// 4. 查询当天所有挂号记录。
|
||||
// available 只由当前有效预约(status=1)决定;has_appointment 保留历史挂号事实,
|
||||
// 避免预约在完成、过号或取消后被误显示为“空号”。
|
||||
$appointmentRows = Appointment::where([
|
||||
'doctor_id' => $doctorId,
|
||||
'appointment_date' => $date,
|
||||
'status' => 1 // 只查询有效预约
|
||||
])->column('appointment_time');
|
||||
])->field(['appointment_time', 'status'])->select()->toArray();
|
||||
|
||||
// 将时间格式统一为 HH:MM(去掉秒)
|
||||
$appointments = array_map(function($time) {
|
||||
// 如果是 HH:MM:SS 格式,截取前5位
|
||||
return substr($time, 0, 5);
|
||||
}, $appointmentTimes);
|
||||
$appointmentMap = [];
|
||||
$activeAppointmentMap = [];
|
||||
foreach ($appointmentRows as $appointmentRow) {
|
||||
// 将 HH:MM:SS 统一为 HH:MM
|
||||
$appointmentTime = substr((string) ($appointmentRow['appointment_time'] ?? ''), 0, 5);
|
||||
if ($appointmentTime === '') {
|
||||
continue;
|
||||
}
|
||||
$appointmentMap[$appointmentTime] = true;
|
||||
if ((int) ($appointmentRow['status'] ?? 0) === 1) {
|
||||
$activeAppointmentMap[$appointmentTime] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 标记已占用的时间段
|
||||
// 5. 分别标记历史挂号与当前占用状态
|
||||
foreach ($slots as &$slot) {
|
||||
if (in_array($slot['time'], $appointments)) {
|
||||
$slot['has_appointment'] = isset($appointmentMap[$slot['time']]);
|
||||
if (isset($activeAppointmentMap[$slot['time']])) {
|
||||
$slot['available'] = false;
|
||||
$slot['quota'] = 0;
|
||||
}
|
||||
}
|
||||
unset($slot);
|
||||
|
||||
// 5. 按时间排序
|
||||
// 6. 按时间排序
|
||||
usort($slots, function($a, $b) {
|
||||
return strcmp($a['time'], $b['time']);
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ class FirstVisitConversionLogic
|
||||
$selectedAssistantId = max(0, (int) ($params['assistant_id'] ?? 0));
|
||||
$requestedMediaChannelCode = trim((string)($params['media_channel_code'] ?? ''));
|
||||
$selectedMediaChannel = $requestedMediaChannelCode !== ''
|
||||
? MediaChannelService::getChannelByCode($requestedMediaChannelCode)
|
||||
? MediaChannelService::getCurrentTagChannelByCode($requestedMediaChannelCode)
|
||||
: null;
|
||||
$selectedMediaChannelCode = $selectedMediaChannel !== null ? $requestedMediaChannelCode : '';
|
||||
|
||||
@@ -79,7 +79,9 @@ class FirstVisitConversionLogic
|
||||
'time_type' => 'custom',
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'include_filters' => 1,
|
||||
// 一诊筛选项在本层按自身权限和“当前企微标签”口径生成,不再让通用
|
||||
// Conversion 额外加载一套包含历史渠道的筛选器。
|
||||
'include_filters' => 0,
|
||||
'include_members' => 1,
|
||||
'exclude_cancelled_appointments' => 1,
|
||||
'order_metric_mode' => 'performance',
|
||||
@@ -98,7 +100,8 @@ class FirstVisitConversionLogic
|
||||
$adminId,
|
||||
$adminInfo,
|
||||
$effectiveAdminIds,
|
||||
$costAllocationAdminIds
|
||||
$costAllocationAdminIds,
|
||||
$selectedMediaChannel
|
||||
);
|
||||
$rows = is_array($conversion['lists'] ?? null) ? $conversion['lists'] : [];
|
||||
$rowAllowedDeptIds = self::visibleRowDeptIds($effectiveAdminIds);
|
||||
@@ -149,10 +152,6 @@ class FirstVisitConversionLogic
|
||||
$selectedMediaChannelName = $selectedMediaChannelCode !== ''
|
||||
? (string) ($selectedMediaChannel['channel_name'] ?? $selectedMediaChannelCode)
|
||||
: '';
|
||||
$conversionFilters = is_array($conversion['extend']['filters'] ?? null)
|
||||
? $conversion['extend']['filters']
|
||||
: [];
|
||||
|
||||
return [
|
||||
'meta' => [
|
||||
'time_type' => $timeType,
|
||||
@@ -176,9 +175,7 @@ class FirstVisitConversionLogic
|
||||
'filters' => [
|
||||
'departments' => DeptLogic::getAllDataScoped($adminId, $adminInfo),
|
||||
'assistants' => self::assistantOptions($baseVisibleAdminIds, $selectedDeptIds, $selectedDeptId),
|
||||
'media_channels' => is_array($conversionFilters['media_channels'] ?? null)
|
||||
? $conversionFilters['media_channels']
|
||||
: [],
|
||||
'media_channels' => MediaChannelService::getCurrentTagOptions(),
|
||||
],
|
||||
'summary' => $summary,
|
||||
'rankings' => [
|
||||
|
||||
@@ -8,6 +8,7 @@ use app\common\logic\BaseLogic;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\QywxExternalContact;
|
||||
use app\common\model\QywxSyncSettings;
|
||||
use app\common\service\qywx\MediaChannelService;
|
||||
use app\common\service\wechat\WechatWorkService;
|
||||
use think\facade\Cache;
|
||||
use think\facade\Db;
|
||||
@@ -306,6 +307,7 @@ class CustomerLogic extends BaseLogic
|
||||
}
|
||||
|
||||
// 4. 更新同步设置
|
||||
MediaChannelService::forgetCurrentTagCatalogCache();
|
||||
self::updateSyncStatus('success', $syncCount);
|
||||
|
||||
Log::info('同步完成 - 总数: ' . $syncCount . ', 新增: ' . $newCount . ', 更新: ' . $updateCount);
|
||||
@@ -650,6 +652,7 @@ class CustomerLogic extends BaseLogic
|
||||
$updateCount,
|
||||
$skippedCount
|
||||
);
|
||||
MediaChannelService::forgetCurrentTagCatalogCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -739,6 +742,7 @@ class CustomerLogic extends BaseLogic
|
||||
Db::name('qywx_external_contact_tag')
|
||||
->where('external_userid', $externalUserId)
|
||||
->delete();
|
||||
MediaChannelService::forgetCurrentTagCatalogCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -796,6 +800,7 @@ class CustomerLogic extends BaseLogic
|
||||
Db::name('qywx_external_contact_tag')
|
||||
->where('external_userid', $externalUserId)
|
||||
->delete();
|
||||
MediaChannelService::forgetCurrentTagCatalogCache();
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -817,6 +822,7 @@ class CustomerLogic extends BaseLogic
|
||||
|
||||
// 关系表按剩余 kept follow_users 同步(自动清掉离开员工那行 + 保留其他员工的标签)
|
||||
self::syncContactTagsRelation($externalUserId, $kept);
|
||||
MediaChannelService::forgetCurrentTagCatalogCache();
|
||||
}
|
||||
|
||||
private static function upsertOneExternalContactBundle(
|
||||
@@ -1137,24 +1143,11 @@ class CustomerLogic extends BaseLogic
|
||||
*/
|
||||
public static function getTagStats(): array
|
||||
{
|
||||
// 关系表里 external_userid 可能指向已被软删的客户;这里 INNER JOIN 主表过滤未删除的
|
||||
$rows = Db::name('qywx_external_contact_tag')
|
||||
->alias('ect')
|
||||
->join('qywx_external_contact ec', 'ec.external_userid = ect.external_userid', 'INNER')
|
||||
->whereNull('ec.delete_time')
|
||||
->field([
|
||||
'ect.tag_id',
|
||||
'ect.tag_name',
|
||||
'ect.group_name',
|
||||
'COUNT(DISTINCT ect.external_userid) AS customer_count',
|
||||
])
|
||||
->group('ect.tag_id, ect.tag_name, ect.group_name')
|
||||
->order('customer_count', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
// 与一诊渠道共用同一份“当前有效标签”投影:一 tag_id 一条最新名称,
|
||||
// 避免企微标签改名后在筛选器里同时出现新旧快照。
|
||||
$rows = MediaChannelService::getCurrentTagCatalog();
|
||||
|
||||
$groupMap = [];
|
||||
$customersUnion = [];
|
||||
foreach ($rows as $r) {
|
||||
$g = (string) ($r['group_name'] ?? '');
|
||||
if (!isset($groupMap[$g])) {
|
||||
|
||||
@@ -32,6 +32,7 @@ class ConversionLogic
|
||||
* @param array $adminInfo 当前 admin 完整信息(含 root / role_id 数组等)
|
||||
* @param int[]|null $trustedVisibleAdminIdsOverride 仅供服务端内部可信调用覆盖本次可见管理员;不从 HTTP 参数读取
|
||||
* @param int[]|null $trustedCostAllocationAdminIdsOverride 仅用于成本按加粉占比分摊的分母,不会放大任何业务指标
|
||||
* @param array<string,mixed>|null $trustedMediaChannelOverride 仅供服务端内部传入已校验渠道,避免再次按全局历史渠道口径解析
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* 数据权限:通过 DataScopeService::getVisibleAdminIds 拿到当前用户的"可见 admin id 集合"。
|
||||
@@ -44,7 +45,8 @@ class ConversionLogic
|
||||
int $adminId = 0,
|
||||
?array $adminInfo = null,
|
||||
?array $trustedVisibleAdminIdsOverride = null,
|
||||
?array $trustedCostAllocationAdminIdsOverride = null
|
||||
?array $trustedCostAllocationAdminIdsOverride = null,
|
||||
?array $trustedMediaChannelOverride = null
|
||||
): array
|
||||
{
|
||||
self::$requestRowsCache = [];
|
||||
@@ -56,9 +58,10 @@ class ConversionLogic
|
||||
$usePerformanceOrderMetrics = strtolower(trim((string)($params['order_metric_mode'] ?? ''))) === 'performance';
|
||||
$dimension = self::normalizeDimension((string)($params['dimension'] ?? 'dept'));
|
||||
$requestedMediaChannelCode = trim((string)($params['media_channel_code'] ?? ''));
|
||||
$mediaChannel = $requestedMediaChannelCode !== ''
|
||||
? MediaChannelService::getChannelByCode($requestedMediaChannelCode)
|
||||
: null;
|
||||
$mediaChannel = $trustedMediaChannelOverride;
|
||||
if ($mediaChannel === null && $requestedMediaChannelCode !== '') {
|
||||
$mediaChannel = MediaChannelService::getChannelByCode($requestedMediaChannelCode);
|
||||
}
|
||||
$mediaChannelCode = $mediaChannel !== null ? $requestedMediaChannelCode : '';
|
||||
$filterEmptyEntities = $mediaChannel !== null;
|
||||
[$startTimestamp, $endTimestamp, $startDate, $endDate] = self::resolveTimeRange($params);
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace app\command;
|
||||
|
||||
use app\adminapi\logic\qywx\CustomerLogic;
|
||||
use app\common\service\qywx\MediaChannelService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
@@ -161,6 +162,7 @@ class QywxBackfillCustomerTags extends Command
|
||||
$output->writeln("空 tags 行数: {$emptyTags} (follow_user 内无任何 tag)");
|
||||
$output->writeln("关系表同步: {$relationSynced} 行");
|
||||
$output->writeln("耗时: {$duration}秒");
|
||||
MediaChannelService::forgetCurrentTagCatalogCache();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -281,6 +283,7 @@ class QywxBackfillCustomerTags extends Command
|
||||
$output->writeln("关系表 INSERT IGNORE: {$tagRowsInserted}(含可能被忽略的重复行)");
|
||||
$output->writeln("tags JSON 批量 UPDATE: {$jsonUpdated}");
|
||||
$output->writeln("耗时: {$duration}秒");
|
||||
MediaChannelService::forgetCurrentTagCatalogCache();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
-- 当前企微标签目录需要按 external_userid 判断客户是否仍有效。
|
||||
-- 覆盖 external_userid + delete_time,避免标签聚合逐条回表读取体积较大的客户 JSON 数据。
|
||||
SET @idx_external_delete_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'zyt_qywx_external_contact'
|
||||
AND INDEX_NAME = 'idx_external_delete'
|
||||
);
|
||||
|
||||
SET @add_idx_external_delete_sql := IF(
|
||||
@idx_external_delete_exists = 0,
|
||||
'ALTER TABLE `zyt_qywx_external_contact` ADD INDEX `idx_external_delete` (`external_userid`, `delete_time`)',
|
||||
'SELECT 1'
|
||||
);
|
||||
|
||||
PREPARE add_idx_external_delete_stmt FROM @add_idx_external_delete_sql;
|
||||
EXECUTE add_idx_external_delete_stmt;
|
||||
DEALLOCATE PREPARE add_idx_external_delete_stmt;
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use app\adminapi\logic\qywx\CustomerLogic;
|
||||
use app\common\service\qywx\MediaChannelService;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
$mergeMethod = new ReflectionMethod(MediaChannelService::class, 'mergeCurrentTagsWithConfiguredChannels');
|
||||
$mergeMethod->setAccessible(true);
|
||||
|
||||
$catalog = [
|
||||
['tag_id' => 'tag-a', 'tag_name' => '最新标签 A', 'group_name' => '当前分组', 'customer_count' => 12],
|
||||
['tag_id' => 'tag-disabled', 'tag_name' => '当前仍在用', 'group_name' => '当前分组', 'customer_count' => 8],
|
||||
['tag_id' => 'tag-new', 'tag_name' => '新发现标签', 'group_name' => '其它', 'customer_count' => 3],
|
||||
];
|
||||
$configured = [
|
||||
[
|
||||
'id' => 1,
|
||||
'channel_code' => 'stable-a',
|
||||
'channel_name' => '人工渠道名',
|
||||
'source_tag_id' => 'tag-a',
|
||||
'source_tag_name' => '旧标签 A',
|
||||
'source_group_name' => '旧分组',
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'channel_code' => 'stable-disabled',
|
||||
'channel_name' => '停用时名称',
|
||||
'source_tag_id' => 'tag-disabled',
|
||||
'source_tag_name' => '停用时名称',
|
||||
'source_group_name' => '旧分组',
|
||||
'status' => 0,
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'channel_code' => 'legacy-name-only',
|
||||
'channel_name' => '历史个人标签',
|
||||
'source_tag_id' => '',
|
||||
'source_tag_name' => '历史个人标签',
|
||||
'source_group_name' => '历史',
|
||||
'status' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
/** @var array<int, array<string, mixed>> $rows */
|
||||
$rows = $mergeMethod->invoke(null, $catalog, $configured);
|
||||
if (count($rows) !== count($catalog)) {
|
||||
throw new RuntimeException('Current-tag projection leaked a historical channel or lost a current tag');
|
||||
}
|
||||
|
||||
$byTagId = [];
|
||||
foreach ($rows as $row) {
|
||||
$byTagId[(string) ($row['source_tag_id'] ?? '')] = $row;
|
||||
}
|
||||
|
||||
$renamed = $byTagId['tag-a'] ?? null;
|
||||
if (!is_array($renamed)
|
||||
|| ($renamed['channel_code'] ?? '') !== 'stable-a'
|
||||
|| ($renamed['channel_name'] ?? '') !== '最新标签 A'
|
||||
|| ($renamed['source_tag_name'] ?? '') !== '最新标签 A'
|
||||
|| ($renamed['source_group_name'] ?? '') !== '当前分组'
|
||||
|| ($renamed['customer_count'] ?? 0) !== 12
|
||||
|| ($renamed['legacy_channel_name'] ?? '') !== '人工渠道名'
|
||||
|| ($renamed['legacy_source_tag_name'] ?? '') !== '旧标签 A') {
|
||||
throw new RuntimeException('Current tag metadata did not override historical display metadata safely');
|
||||
}
|
||||
|
||||
$disabled = $byTagId['tag-disabled'] ?? null;
|
||||
if (!is_array($disabled)
|
||||
|| ($disabled['channel_code'] ?? '') !== 'stable-disabled'
|
||||
|| ($disabled['channel_name'] ?? '') !== '当前仍在用'
|
||||
|| ($disabled['status'] ?? 0) !== 1) {
|
||||
throw new RuntimeException('A current WeCom tag was hidden by historical registry status');
|
||||
}
|
||||
|
||||
$newTag = $byTagId['tag-new'] ?? null;
|
||||
if (!is_array($newTag)
|
||||
|| ($newTag['channel_code'] ?? '') !== 'tag_tag-new'
|
||||
|| ($newTag['channel_name'] ?? '') !== '新发现标签') {
|
||||
throw new RuntimeException('A current unregistered tag did not receive its deterministic channel code');
|
||||
}
|
||||
|
||||
if (isset($byTagId['']) || in_array('legacy-name-only', array_column($rows, 'channel_code'), true)) {
|
||||
throw new RuntimeException('Historical name-only channels must not appear in the current-tag projection');
|
||||
}
|
||||
|
||||
if (in_array('--integration', $argv, true)) {
|
||||
$app = new think\App();
|
||||
$app->initialize();
|
||||
|
||||
$startedAt = microtime(true);
|
||||
$currentCatalog = MediaChannelService::getCurrentTagCatalog();
|
||||
$catalogElapsedMs = round((microtime(true) - $startedAt) * 1000, 1);
|
||||
$optionsStartedAt = microtime(true);
|
||||
$currentOptions = MediaChannelService::getCurrentTagOptions();
|
||||
$optionsElapsedMs = round((microtime(true) - $optionsStartedAt) * 1000, 1);
|
||||
$statsStartedAt = microtime(true);
|
||||
$tagStats = CustomerLogic::getTagStats();
|
||||
$statsElapsedMs = round((microtime(true) - $statsStartedAt) * 1000, 1);
|
||||
$elapsedMs = round((microtime(true) - $startedAt) * 1000, 1);
|
||||
|
||||
$catalogById = [];
|
||||
foreach ($currentCatalog as $tag) {
|
||||
$catalogById[(string) ($tag['tag_id'] ?? '')] = $tag;
|
||||
}
|
||||
$statsById = [];
|
||||
foreach ($tagStats['groups'] ?? [] as $group) {
|
||||
foreach ($group['tags'] ?? [] as $tag) {
|
||||
$statsById[(string) ($tag['tag_id'] ?? '')] = [
|
||||
'tag_name' => (string) ($tag['tag_name'] ?? ''),
|
||||
'group_name' => (string) ($group['group_name'] ?? ''),
|
||||
'customer_count' => (int) ($tag['customer_count'] ?? 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (count($catalogById) !== count($currentCatalog)
|
||||
|| count($currentOptions) !== count($currentCatalog)
|
||||
|| count($statsById) !== count($currentCatalog)) {
|
||||
throw new RuntimeException('Current catalog, qywx tag stats, and first-visit options are not one-to-one');
|
||||
}
|
||||
|
||||
foreach ($currentOptions as $option) {
|
||||
$tagId = (string) ($option['tag_id'] ?? '');
|
||||
$catalogTag = $catalogById[$tagId] ?? null;
|
||||
if (!is_array($catalogTag)
|
||||
|| ($option['name'] ?? '') !== ($catalogTag['tag_name'] ?? '')
|
||||
|| ($option['group_name'] ?? '') !== ($catalogTag['group_name'] ?? '')
|
||||
|| ($option['customer_count'] ?? 0) !== ($catalogTag['customer_count'] ?? 0)
|
||||
|| MediaChannelService::getCurrentTagChannelByCode((string) ($option['code'] ?? '')) === null) {
|
||||
throw new RuntimeException("First-visit option {$tagId} differs from the current qywx tag catalog");
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($catalogById as $tagId => $catalogTag) {
|
||||
$statsTag = $statsById[$tagId] ?? null;
|
||||
if (!is_array($statsTag)
|
||||
|| $statsTag['tag_name'] !== (string) ($catalogTag['tag_name'] ?? '')
|
||||
|| $statsTag['group_name'] !== (string) ($catalogTag['group_name'] ?? '')
|
||||
|| $statsTag['customer_count'] !== (int) ($catalogTag['customer_count'] ?? 0)) {
|
||||
throw new RuntimeException("Qywx tag stats {$tagId} differs from the shared current catalog");
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'current_tag_count' => count($currentCatalog),
|
||||
'first_visit_option_count' => count($currentOptions),
|
||||
'qywx_tag_count' => count($statsById),
|
||||
'matching_4' => array_values(array_map(
|
||||
static fn (array $option): string => (string) ($option['name'] ?? ''),
|
||||
array_filter(
|
||||
$currentOptions,
|
||||
static fn (array $option): bool => mb_strpos((string) ($option['name'] ?? ''), '4') !== false
|
||||
)
|
||||
)),
|
||||
'catalog_ms' => $catalogElapsedMs,
|
||||
'options_ms' => $optionsElapsedMs,
|
||||
'qywx_stats_ms' => $statsElapsedMs,
|
||||
'elapsed_ms' => $elapsedMs,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
|
||||
}
|
||||
|
||||
echo "CURRENT_TAG_CHANNEL_PROJECTION_OK\n";
|
||||
Reference in New Issue
Block a user