This commit is contained in:
Your Name
2026-08-25 11:40:58 +08:00
parent b8ccbaf567
commit f24afa116f
350 changed files with 1910 additions and 371 deletions
@@ -31,49 +31,22 @@ class FirstVisitConversionLogic
/** @return array<string,mixed> */
public static function overview(array $params, int $adminId, array $adminInfo): array
{
[$startDate, $endDate, $timeType, $timeLabel] = self::resolveTimeRange($params);
$baseVisibleAdminIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
$allowedDeptSet = DataScopeService::getAllowedDeptIdSet($adminId, $adminInfo);
$scopeValue = DataScopeService::getEffectiveScope($adminInfo);
$selectedDeptId = max(0, (int) ($params['dept_id'] ?? 0));
$selectedAssistantId = max(0, (int) ($params['assistant_id'] ?? 0));
$requestedMediaChannelCode = trim((string)($params['media_channel_code'] ?? ''));
$selectedMediaChannel = $requestedMediaChannelCode !== ''
? MediaChannelService::getCurrentTagChannelByCode($requestedMediaChannelCode)
: null;
$selectedMediaChannelCode = $selectedMediaChannel !== null ? $requestedMediaChannelCode : '';
$deptSelectionValid = $selectedDeptId <= 0
|| $allowedDeptSet === null
|| isset($allowedDeptSet[$selectedDeptId]);
$selectedDeptIds = [];
if ($selectedDeptId > 0 && $deptSelectionValid) {
$selectedDeptIds = array_values(array_unique(array_filter(array_map(
'intval',
DeptLogic::getSelfAndDescendantIds($selectedDeptId)
), static fn (int $id): bool => $id > 0)));
if ($allowedDeptSet !== null) {
$selectedDeptIds = array_values(array_filter(
$selectedDeptIds,
static fn (int $id): bool => isset($allowedDeptSet[$id])
));
}
}
$effectiveAdminIds = $deptSelectionValid ? $baseVisibleAdminIds : [];
if ($selectedDeptId > 0 && $deptSelectionValid) {
$deptAdminIds = $selectedDeptIds === []
? []
: self::normalizeIds(AdminDept::whereIn('dept_id', $selectedDeptIds)->column('admin_id'));
$effectiveAdminIds = self::intersectVisibleIds($effectiveAdminIds, $deptAdminIds);
}
$selectedAssistantValid = $selectedAssistantId <= 0;
if ($selectedAssistantId > 0) {
$selectedAssistantValid = self::isActiveAssistant($selectedAssistantId)
&& ($effectiveAdminIds === null || in_array($selectedAssistantId, $effectiveAdminIds, true));
$effectiveAdminIds = $selectedAssistantValid ? [$selectedAssistantId] : [];
}
$context = self::resolveScopeContext($params, $adminId, $adminInfo);
$startDate = $context['start_date'];
$endDate = $context['end_date'];
$timeType = $context['time_type'];
$timeLabel = $context['time_label'];
$allowedDeptSet = $context['allowed_dept_set'];
$baseVisibleAdminIds = $context['base_visible_admin_ids'];
$scopeValue = $context['scope_value'];
$selectedDeptId = $context['selected_dept_id'];
$selectedAssistantId = $context['selected_assistant_id'];
$selectedMediaChannel = $context['selected_media_channel'];
$selectedMediaChannelCode = $context['selected_media_channel_code'];
$deptSelectionValid = $context['dept_selection_valid'];
$selectedDeptIds = $context['selected_dept_ids'];
$effectiveAdminIds = $context['effective_admin_ids'];
$selectedAssistantValid = $context['selected_assistant_valid'];
$costAllocationAdminIds = self::costAllocationAdminIds(
$effectiveAdminIds,
$scopeValue === DataScopeService::SCOPE_SELF || $selectedAssistantId > 0
@@ -208,6 +181,337 @@ class FirstVisitConversionLogic
];
}
/** @return array<string,mixed> */
public static function fansDetail(array $params, int $adminId, array $adminInfo): array
{
$context = self::resolveScopeContext($params, $adminId, $adminInfo);
$context['channel_dept_ids'] = ConversionLogic::fanDetailChannelDeptIds(
$context['selected_media_channel']
);
$pageNo = max(1, (int) ($params['page_no'] ?? 1));
$pageSize = max(1, min(100, (int) ($params['page_size'] ?? 20)));
$empty = [
'lists' => [],
'count' => 0,
'page_no' => $pageNo,
'page_size' => $pageSize,
'date_range' => [$context['start_date'], $context['end_date']],
'entity' => null,
];
$entityType = strtolower(trim((string) ($params['entity_type'] ?? '')));
if (!in_array($entityType, ['dept', 'member'], true)) {
return $empty;
}
$entityId = trim((string) ($params['entity_id'] ?? ''));
$requestedAdminId = max(0, (int) ($params['admin_id'] ?? 0));
$resolved = self::resolveFanDetailEntity(
$entityType,
$entityId,
$requestedAdminId,
$context
);
if ($resolved === null) {
return $empty;
}
$target = $resolved['target'];
$conversionParams = [
'dimension' => 'dept',
'time_type' => 'custom',
'start_date' => $context['start_date'],
'end_date' => $context['end_date'],
'page_no' => $pageNo,
'page_size' => $pageSize,
];
if ((int) $context['selected_dept_id'] > 0 && !empty($context['dept_selection_valid'])) {
$conversionParams['dept_id'] = (int) $context['selected_dept_id'];
}
if ((string) $context['selected_media_channel_code'] !== '') {
$conversionParams['media_channel_code'] = (string) $context['selected_media_channel_code'];
}
$result = ConversionLogic::fanDetails(
$conversionParams,
$target,
$adminId,
$adminInfo,
$context['effective_admin_ids'],
$context['selected_media_channel']
);
if ((int) ($result['count'] ?? 0) <= 0) {
return $empty;
}
$result['entity'] = [
'type' => $entityType,
'id' => $resolved['id'],
'admin_id' => (int) $resolved['admin_id'],
'name' => (string) $resolved['name'],
'add_fans_count' => (int) $result['count'],
'deleted_fans_count' => (int) ($result['deleted_count'] ?? 0),
];
unset($result['deleted_count']);
return $result;
}
/**
* Resolve only the clicked entity and its authorized target range. This is
* deliberately structural: it avoids recomputing all overview metrics,
* while still rejecting ids that do not exist in the current DataScope.
*
* @param array<string,mixed> $context
* @return array{id:string|int,admin_id:int,name:string,target:array<string,mixed>}|null
*/
private static function resolveFanDetailEntity(
string $entityType,
string $entityId,
int $requestedAdminId,
array $context
): ?array {
$visibleAdminIds = $context['effective_admin_ids'];
$selectedDeptId = (int) $context['selected_dept_id'];
$channelDeptIds = $context['channel_dept_ids'] ?? null;
if ($entityType === 'member' && str_starts_with($entityId, 'U_')) {
$wecomUserId = trim(substr($entityId, 2));
if ($wecomUserId === '' || $requestedAdminId > 0 || $visibleAdminIds !== null || $selectedDeptId > 0) {
return null;
}
$isBound = Db::name('admin')
->whereNull('delete_time')
->where('work_wechat_userid', $wecomUserId)
->count() > 0;
if ($isBound) {
return null;
}
$historicalName = trim((string) (Db::name('admin')
->where('work_wechat_userid', $wecomUserId)
->order('id', 'desc')
->value('name') ?? ''));
return [
'id' => $entityId,
'admin_id' => 0,
'name' => $historicalName !== ''
? $historicalName . '' . $wecomUserId . ''
: $wecomUserId,
'target' => ['type' => 'wecom_user', 'wecom_userid' => $wecomUserId],
];
}
if ($entityType === 'member') {
if (!preg_match('/^M([1-9]\d*)_(-?\d+)$/', $entityId, $matches)) {
return null;
}
$targetAdminId = (int) $matches[1];
$rowDeptId = (int) $matches[2];
if (($requestedAdminId > 0 && $requestedAdminId !== $targetAdminId)
|| ($visibleAdminIds !== null && !in_array($targetAdminId, $visibleAdminIds, true))
) {
return null;
}
$admin = Db::name('admin')
->where('id', $targetAdminId)
->whereNull('delete_time')
->where('work_wechat_userid', '<>', '')
->field('id,name')
->find();
$resolvedDeptId = self::resolveFanDetailMemberDeptId($targetAdminId, $context);
if (!$admin
|| $resolvedDeptId !== $rowDeptId
|| ($resolvedDeptId === -2 && $selectedDeptId > 0)
) {
return null;
}
return [
'id' => $entityId,
'admin_id' => $targetAdminId,
'name' => trim((string) ($admin['name'] ?? '')),
'target' => ['type' => 'member', 'admin_id' => $targetAdminId],
];
}
if ($entityType !== 'dept' || !preg_match('/^-?\d+$/', $entityId)) {
return null;
}
$deptId = (int) $entityId;
if (in_array($deptId, [-1, -2], true)) {
if ($selectedDeptId > 0 || $visibleAdminIds !== null) {
return null;
}
$name = $deptId === -1 ? '未绑定后台账号' : '未分配部门';
return [
'id' => $deptId,
'admin_id' => 0,
'name' => $name,
'target' => ['type' => 'dept', 'dept_ids' => [$deptId]],
];
}
if ($deptId <= 0) {
return null;
}
$dept = Db::name('dept')->where('id', $deptId)->whereNull('delete_time')->field('id,name')->find();
if (!$dept) {
return null;
}
$universeDeptIds = $selectedDeptId > 0
? self::normalizeIds(DeptLogic::getSelfAndDescendantIds($selectedDeptId))
: self::normalizeIds(Db::name('dept')->whereNull('delete_time')->column('id'));
if ($channelDeptIds !== null) {
$universeDeptIds = array_values(array_intersect($universeDeptIds, $channelDeptIds));
}
$rowDeptIds = self::visibleRowDeptIds($visibleAdminIds);
if (!in_array($deptId, $universeDeptIds, true)
|| ($rowDeptIds !== null && !in_array($deptId, $rowDeptIds, true))
) {
return null;
}
$targetDeptIds = array_values(array_intersect(
self::normalizeIds(DeptLogic::getSelfAndDescendantIds($deptId)),
$universeDeptIds,
$rowDeptIds ?? $universeDeptIds
));
if ($targetDeptIds === []) {
return null;
}
return [
'id' => $deptId,
'admin_id' => 0,
'name' => trim((string) ($dept['name'] ?? '')),
'target' => ['type' => 'dept', 'dept_ids' => $targetDeptIds],
];
}
/** @param array<string,mixed> $context */
private static function resolveFanDetailMemberDeptId(int $adminId, array $context): int
{
// Other roles are rendered by ConversionLogic in the unassigned bucket.
$hasMedicalRole = Db::name('admin_role')
->where('admin_id', $adminId)
->whereIn('role_id', [1, self::ASSISTANT_ROLE_ID])
->count() > 0;
if (!$hasMedicalRole) {
return -2;
}
$selectedDeptId = (int) $context['selected_dept_id'];
$validDeptIds = $selectedDeptId > 0
? self::normalizeIds(DeptLogic::getSelfAndDescendantIds($selectedDeptId))
: self::normalizeIds(Db::name('dept')->whereNull('delete_time')->column('id'));
if (($context['channel_dept_ids'] ?? null) !== null) {
$validDeptIds = array_values(array_intersect($validDeptIds, $context['channel_dept_ids']));
}
$validDeptSet = array_fill_keys($validDeptIds, true);
$deptRows = Db::name('dept')->whereNull('delete_time')->field('id,pid,sort')->select()->toArray();
$deptMeta = [];
foreach ($deptRows as $deptRow) {
$id = (int) ($deptRow['id'] ?? 0);
if ($id > 0) {
$deptMeta[$id] = ['pid' => (int) ($deptRow['pid'] ?? 0), 'sort' => (int) ($deptRow['sort'] ?? 0)];
}
}
$depthCache = [];
$depthOf = static function (int $id) use (&$depthOf, &$depthCache, $deptMeta): int {
if ($id <= 0 || !isset($deptMeta[$id])) {
return 0;
}
if (isset($depthCache[$id])) {
return $depthCache[$id];
}
$pid = (int) ($deptMeta[$id]['pid'] ?? 0);
if ($pid <= 0 || $pid === $id || !isset($deptMeta[$pid])) {
return $depthCache[$id] = 0;
}
return $depthCache[$id] = $depthOf($pid) + 1;
};
$adminDeptIds = self::normalizeIds(AdminDept::where('admin_id', $adminId)->column('dept_id'));
usort($adminDeptIds, static function (int $left, int $right) use ($depthOf, $deptMeta): int {
return ($depthOf($right) <=> $depthOf($left))
?: ((int) ($deptMeta[$right]['sort'] ?? 0) <=> (int) ($deptMeta[$left]['sort'] ?? 0))
?: ($left <=> $right);
});
foreach ($adminDeptIds as $adminDeptId) {
if (isset($validDeptSet[$adminDeptId])) {
return $adminDeptId;
}
}
return -2;
}
/** @return array<string,mixed> */
private static function resolveScopeContext(array $params, int $adminId, array $adminInfo): array
{
[$startDate, $endDate, $timeType, $timeLabel] = self::resolveTimeRange($params);
$baseVisibleAdminIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
$allowedDeptSet = DataScopeService::getAllowedDeptIdSet($adminId, $adminInfo);
$scopeValue = DataScopeService::getEffectiveScope($adminInfo);
$selectedDeptId = max(0, (int) ($params['dept_id'] ?? 0));
$selectedAssistantId = max(0, (int) ($params['assistant_id'] ?? 0));
$requestedMediaChannelCode = trim((string) ($params['media_channel_code'] ?? ''));
$selectedMediaChannel = $requestedMediaChannelCode !== ''
? MediaChannelService::getCurrentTagChannelByCode($requestedMediaChannelCode)
: null;
$selectedMediaChannelCode = $selectedMediaChannel !== null ? $requestedMediaChannelCode : '';
$deptSelectionValid = $selectedDeptId <= 0
|| $allowedDeptSet === null
|| isset($allowedDeptSet[$selectedDeptId]);
$selectedDeptIds = [];
if ($selectedDeptId > 0 && $deptSelectionValid) {
$selectedDeptIds = array_values(array_unique(array_filter(array_map(
'intval',
DeptLogic::getSelfAndDescendantIds($selectedDeptId)
), static fn (int $id): bool => $id > 0)));
if ($allowedDeptSet !== null) {
$selectedDeptIds = array_values(array_filter(
$selectedDeptIds,
static fn (int $id): bool => isset($allowedDeptSet[$id])
));
}
}
$effectiveAdminIds = $deptSelectionValid ? $baseVisibleAdminIds : [];
if ($selectedDeptId > 0 && $deptSelectionValid) {
$deptAdminIds = $selectedDeptIds === []
? []
: self::normalizeIds(AdminDept::whereIn('dept_id', $selectedDeptIds)->column('admin_id'));
$effectiveAdminIds = self::intersectVisibleIds($effectiveAdminIds, $deptAdminIds);
}
$selectedAssistantValid = $selectedAssistantId <= 0;
if ($selectedAssistantId > 0) {
$selectedAssistantValid = self::isActiveAssistant($selectedAssistantId)
&& ($effectiveAdminIds === null || in_array($selectedAssistantId, $effectiveAdminIds, true));
$effectiveAdminIds = $selectedAssistantValid ? [$selectedAssistantId] : [];
}
return [
'start_date' => $startDate,
'end_date' => $endDate,
'time_type' => $timeType,
'time_label' => $timeLabel,
'base_visible_admin_ids' => $baseVisibleAdminIds,
'allowed_dept_set' => $allowedDeptSet,
'scope_value' => $scopeValue,
'selected_dept_id' => $selectedDeptId,
'selected_assistant_id' => $selectedAssistantId,
'selected_media_channel' => $selectedMediaChannel,
'selected_media_channel_code' => $selectedMediaChannelCode,
'dept_selection_valid' => $deptSelectionValid,
'selected_dept_ids' => $selectedDeptIds,
'effective_admin_ids' => $effectiveAdminIds,
'selected_assistant_valid' => $selectedAssistantValid,
];
}
/** @return array{0:string,1:string,2:string,3:string} */
private static function resolveTimeRange(array $params): array
{