增加数据权限
增加调试登录
This commit is contained in:
@@ -7,6 +7,7 @@ namespace app\adminapi\logic\stats;
|
||||
use app\adminapi\logic\dept\DeptLogic;
|
||||
use app\adminapi\logic\tcm\DiagnosisLogic;
|
||||
use app\common\model\finance\AccountCost;
|
||||
use app\common\service\DataScope\DataScopeService;
|
||||
use app\common\service\qywx\MediaChannelService;
|
||||
use think\facade\Db;
|
||||
|
||||
@@ -16,9 +17,17 @@ class ConversionLogic
|
||||
private const VIRTUAL_DEPT_UNASSIGNED_ID = -2;
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
* @param int $adminId 当前操作 admin(来自 BaseAdminController)
|
||||
* @param array $adminInfo 当前 admin 完整信息(含 root / role_id 数组等)
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* 数据权限:通过 DataScopeService::getVisibleAdminIds 拿到当前用户的"可见 admin id 集合"。
|
||||
* - null:SCOPE_ALL,全数据放行(与历史行为一致)
|
||||
* - []:可见为空(SCOPE_SELF 且无绑定且关闭 fallback),返回空数据
|
||||
* - 其他:用 visibleAdminIds 收窄 entities 加载、hydrate 数据归属、虚拟桶可见性、filters 选项
|
||||
*/
|
||||
public static function overview(array $params = []): array
|
||||
public static function overview(array $params = [], int $adminId = 0, ?array $adminInfo = null): array
|
||||
{
|
||||
$includeFilters = (int)($params['include_filters'] ?? 0) === 1;
|
||||
$dimension = self::normalizeDimension((string)($params['dimension'] ?? 'dept'));
|
||||
@@ -29,9 +38,37 @@ class ConversionLogic
|
||||
$pageNo = max(1, (int)($params['page_no'] ?? 1));
|
||||
$pageSize = max(1, min(100, (int)($params['page_size'] ?? 15)));
|
||||
|
||||
$entities = self::loadEntities($dimension, $params);
|
||||
$visibleAdminIds = ($adminInfo !== null && $adminId > 0)
|
||||
? DataScopeService::getVisibleAdminIds($adminId, $adminInfo)
|
||||
: null;
|
||||
// 严格隔离:可见 admin 集合为空时直接返回空骨架,避免下游误以为是"全部"。
|
||||
if ($visibleAdminIds === []) {
|
||||
$emptyResult = [
|
||||
'dimension' => $dimension,
|
||||
'date_range' => [$startDate, $endDate],
|
||||
'summary' => self::buildSummary([]),
|
||||
'charts' => self::buildCharts([]),
|
||||
'lists' => [],
|
||||
'count' => 0,
|
||||
'page_no' => $pageNo,
|
||||
'page_size' => $pageSize,
|
||||
'extend' => [
|
||||
'dimension' => $dimension,
|
||||
'date_range' => [$startDate, $endDate],
|
||||
'summary' => self::buildSummary([]),
|
||||
'charts' => self::buildCharts([]),
|
||||
],
|
||||
];
|
||||
if ($includeFilters) {
|
||||
$emptyResult['extend']['filters'] = self::buildFilterOptions([], []);
|
||||
}
|
||||
|
||||
return $emptyResult;
|
||||
}
|
||||
|
||||
$entities = self::loadEntities($dimension, $params, $visibleAdminIds);
|
||||
$entityIds = array_keys($entities);
|
||||
$allocationEntities = self::loadEntities($dimension, $params);
|
||||
$allocationEntities = self::loadEntities($dimension, $params, $visibleAdminIds);
|
||||
$allocationEntityIds = array_keys($allocationEntities);
|
||||
|
||||
if ($entityIds === []) {
|
||||
@@ -52,18 +89,20 @@ class ConversionLogic
|
||||
],
|
||||
];
|
||||
if ($includeFilters) {
|
||||
$result['extend']['filters'] = self::buildFilterOptions();
|
||||
$result['extend']['filters'] = self::buildFilterOptions($visibleAdminIds, [], $adminId, $adminInfo);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$adminToDeptIds = self::loadAdminDeptMap();
|
||||
self::hydrateFanStats($allocationEntities, $dimension, $allocationEntityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
|
||||
self::hydrateFanStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
|
||||
self::hydrateAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel);
|
||||
self::hydrateOrderAndAmountStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
|
||||
[$globalAccountCost, $accountCostDeptIds] = self::hydrateAccountCostStats($entities, $startDate, $endDate, $mediaChannelCode);
|
||||
self::hydrateFanStats($allocationEntities, $dimension, $allocationEntityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
|
||||
self::hydrateFanStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
|
||||
self::hydrateAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel, $visibleAdminIds);
|
||||
self::hydrateOrderAndAmountStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
|
||||
// 数据隔离:可见部门 = 可见 admin 所属部门并集;用于 account_cost 与下游 cost 分摊。
|
||||
$visibleDeptIds = self::resolveVisibleDeptIds($visibleAdminIds);
|
||||
[$globalAccountCost, $accountCostDeptIds] = self::hydrateAccountCostStats($entities, $startDate, $endDate, $mediaChannelCode, $visibleDeptIds);
|
||||
$supportsDeptBinding = AccountCost::supportsDeptBinding();
|
||||
$restrictAccountCostByDept = $supportsDeptBinding;
|
||||
$restrictStatsByDept = $supportsDeptBinding && $mediaChannelCode !== '';
|
||||
@@ -98,7 +137,7 @@ class ConversionLogic
|
||||
],
|
||||
];
|
||||
if ($includeFilters) {
|
||||
$result['extend']['filters'] = self::buildFilterOptions();
|
||||
$result['extend']['filters'] = self::buildFilterOptions($visibleAdminIds, $eligibleDeptIds, $adminId, $adminInfo);
|
||||
}
|
||||
|
||||
return $result;
|
||||
@@ -142,7 +181,8 @@ class ConversionLogic
|
||||
$eligibleDeptIds,
|
||||
$adminToDeptIds,
|
||||
$validDeptIds,
|
||||
$globalAccountCost
|
||||
$globalAccountCost,
|
||||
$visibleAdminIds
|
||||
);
|
||||
$pagedRows = self::attachDeptMembers($pagedRows, $memberRowsByDeptId);
|
||||
}
|
||||
@@ -164,7 +204,7 @@ class ConversionLogic
|
||||
],
|
||||
];
|
||||
if ($includeFilters) {
|
||||
$result['extend']['filters'] = self::buildFilterOptions();
|
||||
$result['extend']['filters'] = self::buildFilterOptions($visibleAdminIds, $eligibleDeptIds, $adminId, $adminInfo);
|
||||
}
|
||||
|
||||
return $result;
|
||||
@@ -199,25 +239,194 @@ class ConversionLogic
|
||||
],
|
||||
];
|
||||
if ($includeFilters) {
|
||||
$result['extend']['filters'] = self::buildFilterOptions();
|
||||
$result['extend']['filters'] = self::buildFilterOptions($visibleAdminIds, $eligibleDeptIds, $adminId, $adminInfo);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[]|null $visibleAdminIds 可见 admin 集合(null = SCOPE_ALL)
|
||||
* @param int[] $eligibleDeptIds 当媒体渠道筛选生效时收窄过的部门作用域
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function buildFilterOptions(): array
|
||||
private static function buildFilterOptions(
|
||||
?array $visibleAdminIds = null,
|
||||
array $eligibleDeptIds = [],
|
||||
int $adminId = 0,
|
||||
?array $adminInfo = null
|
||||
): array
|
||||
{
|
||||
return [
|
||||
'departments' => DeptLogic::getAllData(),
|
||||
'assistants' => DiagnosisLogic::getAssistants(),
|
||||
'doctors' => DiagnosisLogic::getDoctors(),
|
||||
'media_channels' => MediaChannelService::getOptions(),
|
||||
'departments' => self::buildDepartmentOptions($visibleAdminIds, $eligibleDeptIds),
|
||||
'assistants' => DiagnosisLogic::getAssistants($adminId, $adminInfo),
|
||||
'doctors' => self::buildDoctorOptions($visibleAdminIds),
|
||||
'media_channels' => self::buildMediaChannelOptions($visibleAdminIds, $eligibleDeptIds),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门下拉:
|
||||
* - SCOPE_ALL(visibleAdminIds = null):全部启用部门树
|
||||
* - 其他:仅展示"可见 admin 所属部门 + 媒体渠道作用域部门"的并集,并保留祖先链以维持树形结构
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function buildDepartmentOptions(?array $visibleAdminIds, array $eligibleDeptIds): array
|
||||
{
|
||||
$allTree = DeptLogic::getAllData();
|
||||
if ($visibleAdminIds === null) {
|
||||
return $allTree;
|
||||
}
|
||||
|
||||
$allowedDeptIds = [];
|
||||
if ($visibleAdminIds !== []) {
|
||||
$deptIds = Db::name('admin_dept')
|
||||
->whereIn('admin_id', $visibleAdminIds)
|
||||
->column('dept_id');
|
||||
foreach ($deptIds as $deptId) {
|
||||
$deptId = (int)$deptId;
|
||||
if ($deptId > 0) {
|
||||
$allowedDeptIds[$deptId] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($eligibleDeptIds as $deptId) {
|
||||
$deptId = (int)$deptId;
|
||||
if ($deptId > 0) {
|
||||
$allowedDeptIds[$deptId] = true;
|
||||
}
|
||||
}
|
||||
if ($allowedDeptIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return self::filterDeptTreeByIds($allTree, array_keys($allowedDeptIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 医生下拉:按 visibleAdminIds 收窄。
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function buildDoctorOptions(?array $visibleAdminIds): array
|
||||
{
|
||||
$query = Db::name('admin')
|
||||
->alias('a')
|
||||
->join('admin_role ar', 'a.id = ar.admin_id')
|
||||
->where('ar.role_id', 1)
|
||||
->where('a.disable', 0)
|
||||
->whereNull('a.delete_time');
|
||||
if ($visibleAdminIds !== null) {
|
||||
if ($visibleAdminIds === []) {
|
||||
return [];
|
||||
}
|
||||
$query->whereIn('a.id', $visibleAdminIds);
|
||||
}
|
||||
|
||||
return $query
|
||||
->field(['a.id', 'a.name', 'a.account'])
|
||||
->order('a.id', 'asc')
|
||||
->distinct(true)
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 媒体渠道下拉:按可见部门绑定的渠道收窄。
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function buildMediaChannelOptions(?array $visibleAdminIds, array $eligibleDeptIds): array
|
||||
{
|
||||
$allOptions = MediaChannelService::getOptions();
|
||||
if ($visibleAdminIds === null) {
|
||||
return $allOptions;
|
||||
}
|
||||
if (!AccountCost::supportsDeptBinding()) {
|
||||
return $allOptions;
|
||||
}
|
||||
|
||||
$allowedDeptIds = [];
|
||||
if ($visibleAdminIds !== []) {
|
||||
$deptIds = Db::name('admin_dept')
|
||||
->whereIn('admin_id', $visibleAdminIds)
|
||||
->column('dept_id');
|
||||
foreach ($deptIds as $deptId) {
|
||||
$deptId = (int)$deptId;
|
||||
if ($deptId > 0) {
|
||||
$allowedDeptIds[$deptId] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($eligibleDeptIds as $deptId) {
|
||||
$deptId = (int)$deptId;
|
||||
if ($deptId > 0) {
|
||||
$allowedDeptIds[$deptId] = true;
|
||||
}
|
||||
}
|
||||
if ($allowedDeptIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$allowedCodes = Db::name('account_cost')
|
||||
->whereIn('dept_id', array_keys($allowedDeptIds))
|
||||
->where('media_channel_code', '<>', '')
|
||||
->distinct(true)
|
||||
->column('media_channel_code');
|
||||
$allowedCodeSet = array_fill_keys(array_filter(array_map('strval', $allowedCodes)), true);
|
||||
if ($allowedCodeSet === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter($allOptions, static function ($option) use ($allowedCodeSet): bool {
|
||||
$code = is_array($option) ? (string)($option['code'] ?? '') : '';
|
||||
|
||||
return $code !== '' && isset($allowedCodeSet[$code]);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 dept_id 集合裁剪部门树,自动保留命中节点的祖先链以保证树形完整。
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $tree
|
||||
* @param int[] $allowedIds
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function filterDeptTreeByIds(array $tree, array $allowedIds): array
|
||||
{
|
||||
$allowedSet = array_fill_keys($allowedIds, true);
|
||||
$walker = function (array $node) use (&$walker, $allowedSet): ?array {
|
||||
$children = [];
|
||||
foreach ($node['children'] ?? [] as $child) {
|
||||
if (!is_array($child)) {
|
||||
continue;
|
||||
}
|
||||
$kept = $walker($child);
|
||||
if ($kept !== null) {
|
||||
$children[] = $kept;
|
||||
}
|
||||
}
|
||||
$selfHit = isset($allowedSet[(int)($node['id'] ?? 0)]);
|
||||
if ($selfHit || $children !== []) {
|
||||
$node['children'] = $children;
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
$result = [];
|
||||
foreach ($tree as $node) {
|
||||
$kept = $walker($node);
|
||||
if ($kept !== null) {
|
||||
$result[] = $kept;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function normalizeDimension(string $dimension): string
|
||||
{
|
||||
return in_array($dimension, ['dept', 'assistant', 'doctor'], true) ? $dimension : 'dept';
|
||||
@@ -271,18 +480,19 @@ class ConversionLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[]|null $visibleAdminIds 可见 admin 集合(null = SCOPE_ALL);用于 entities 加载与虚拟桶可见性
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function loadEntities(string $dimension, array $params): array
|
||||
private static function loadEntities(string $dimension, array $params, ?array $visibleAdminIds = null): array
|
||||
{
|
||||
$entities = match ($dimension) {
|
||||
'assistant' => self::loadAdminEntities(2, isset($params['assistant_id']) ? (int)$params['assistant_id'] : 0),
|
||||
'doctor' => self::loadAdminEntities(1, isset($params['doctor_id']) ? (int)$params['doctor_id'] : 0),
|
||||
'assistant' => self::loadAdminEntities(2, isset($params['assistant_id']) ? (int)$params['assistant_id'] : 0, $visibleAdminIds),
|
||||
'doctor' => self::loadAdminEntities(1, isset($params['doctor_id']) ? (int)$params['doctor_id'] : 0, $visibleAdminIds),
|
||||
default => self::loadDeptEntities(isset($params['dept_id']) ? (int)$params['dept_id'] : 0),
|
||||
};
|
||||
|
||||
if ($dimension === 'dept' && (int)($params['dept_id'] ?? 0) <= 0) {
|
||||
self::appendVirtualDeptEntities($entities);
|
||||
self::appendVirtualDeptEntities($entities, $visibleAdminIds);
|
||||
}
|
||||
|
||||
return $entities;
|
||||
@@ -348,9 +558,10 @@ class ConversionLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[]|null $visibleAdminIds 可见 admin 集合(null = SCOPE_ALL,不收窄)
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function loadAdminEntities(int $roleId, int $adminId = 0): array
|
||||
private static function loadAdminEntities(int $roleId, int $adminId = 0, ?array $visibleAdminIds = null): array
|
||||
{
|
||||
$query = Db::name('admin')
|
||||
->alias('a')
|
||||
@@ -362,6 +573,12 @@ class ConversionLogic
|
||||
if ($adminId > 0) {
|
||||
$query->where('a.id', $adminId);
|
||||
}
|
||||
if ($visibleAdminIds !== null) {
|
||||
if ($visibleAdminIds === []) {
|
||||
return [];
|
||||
}
|
||||
$query->whereIn('a.id', $visibleAdminIds);
|
||||
}
|
||||
|
||||
$rows = $query->order('a.id asc')->select()->toArray();
|
||||
$entities = [];
|
||||
@@ -406,17 +623,21 @@ class ConversionLogic
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $entities
|
||||
* @param int[]|null $visibleAdminIds 可见 admin 集合;非 null(即非 SCOPE_ALL)时不挂"未绑定后台账号"虚拟桶,
|
||||
* 因为这些加粉无主,不属于任何 admin 的可见范围。
|
||||
*/
|
||||
private static function appendVirtualDeptEntities(array &$entities): void
|
||||
private static function appendVirtualDeptEntities(array &$entities, ?array $visibleAdminIds = null): void
|
||||
{
|
||||
$entities[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID] = array_merge(
|
||||
self::newEntityRow(self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID, '未绑定后台账号'),
|
||||
[
|
||||
'pid' => 0,
|
||||
'sort' => -999998,
|
||||
'_virtual_bucket' => true,
|
||||
]
|
||||
);
|
||||
if ($visibleAdminIds === null) {
|
||||
$entities[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID] = array_merge(
|
||||
self::newEntityRow(self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID, '未绑定后台账号'),
|
||||
[
|
||||
'pid' => 0,
|
||||
'sort' => -999998,
|
||||
'_virtual_bucket' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$entities[self::VIRTUAL_DEPT_UNASSIGNED_ID] = array_merge(
|
||||
self::newEntityRow(self::VIRTUAL_DEPT_UNASSIGNED_ID, '未分配部门'),
|
||||
@@ -428,6 +649,37 @@ class ConversionLogic
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把"可见 admin 集合"转换成"可见部门集合":取所有 admin 的 admin_dept 并集。
|
||||
* - $visibleAdminIds === null(SCOPE_ALL)→ 返回 null(下游不收窄)
|
||||
* - $visibleAdminIds === [] → 返回 [](无可见部门)
|
||||
*
|
||||
* @param int[]|null $visibleAdminIds
|
||||
* @return int[]|null
|
||||
*/
|
||||
private static function resolveVisibleDeptIds(?array $visibleAdminIds): ?array
|
||||
{
|
||||
if ($visibleAdminIds === null) {
|
||||
return null;
|
||||
}
|
||||
if ($visibleAdminIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$deptIds = Db::name('admin_dept')
|
||||
->whereIn('admin_id', $visibleAdminIds)
|
||||
->column('dept_id');
|
||||
$result = [];
|
||||
foreach ($deptIds as $deptId) {
|
||||
$deptId = (int)$deptId;
|
||||
if ($deptId > 0) {
|
||||
$result[$deptId] = $deptId;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int[]>
|
||||
*
|
||||
@@ -557,6 +809,7 @@ class ConversionLogic
|
||||
* @param int[] $entityIds
|
||||
* @param array<int, int[]> $adminToDeptIds
|
||||
* @param array<string, mixed>|null $mediaChannel
|
||||
* @param int[]|null $visibleAdminIds 可见 admin 集合
|
||||
*/
|
||||
private static function hydrateFanStats(
|
||||
array &$entities,
|
||||
@@ -565,7 +818,8 @@ class ConversionLogic
|
||||
array $adminToDeptIds,
|
||||
int $startTimestamp,
|
||||
int $endTimestamp,
|
||||
?array $mediaChannel
|
||||
?array $mediaChannel,
|
||||
?array $visibleAdminIds = null
|
||||
): void {
|
||||
$query = Db::name('qywx_external_contact_event')
|
||||
->alias('e')
|
||||
@@ -587,7 +841,8 @@ class ConversionLogic
|
||||
$addFansCount = (int)($row['add_fans_count'] ?? 0);
|
||||
|
||||
if ($dimension === 'dept' && $adminId <= 0) {
|
||||
if (isset($entities[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID])) {
|
||||
// -1 桶仅在 SCOPE_ALL(visibleAdminIds === null)时存在;其他范围下未绑定 admin 的加粉不可归属,跳过。
|
||||
if ($visibleAdminIds === null && isset($entities[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID])) {
|
||||
$entities[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID]['add_fans_count'] += $addFansCount;
|
||||
}
|
||||
continue;
|
||||
@@ -597,8 +852,12 @@ class ConversionLogic
|
||||
continue;
|
||||
}
|
||||
|
||||
$hitEntityIds = self::mapEntityIds($dimension, $adminId, $entityIds, $adminToDeptIds);
|
||||
$hitEntityIds = self::mapEntityIds($dimension, $adminId, $entityIds, $adminToDeptIds, $visibleAdminIds);
|
||||
if ($dimension === 'dept' && $hitEntityIds === []) {
|
||||
// admin 已通过 visibleAdminIds 校验但没找到归属部门 → 落入"未分配部门"。
|
||||
if ($visibleAdminIds !== null && !in_array($adminId, $visibleAdminIds, true)) {
|
||||
continue;
|
||||
}
|
||||
if (isset($entities[self::VIRTUAL_DEPT_UNASSIGNED_ID])) {
|
||||
$entities[self::VIRTUAL_DEPT_UNASSIGNED_ID]['add_fans_count'] += $addFansCount;
|
||||
}
|
||||
@@ -620,6 +879,7 @@ class ConversionLogic
|
||||
* @param int[] $entityIds
|
||||
* @param array<int, int[]> $adminToDeptIds
|
||||
* @param array<string, mixed>|null $mediaChannel
|
||||
* @param int[]|null $visibleAdminIds
|
||||
*/
|
||||
private static function hydrateAppointmentStats(
|
||||
array &$entities,
|
||||
@@ -630,7 +890,8 @@ class ConversionLogic
|
||||
int $endTimestamp,
|
||||
string $startDate,
|
||||
string $endDate,
|
||||
?array $mediaChannel
|
||||
?array $mediaChannel,
|
||||
?array $visibleAdminIds = null
|
||||
): void {
|
||||
$sourceExpr = $dimension === 'doctor' ? 'a.doctor_id' : 'u.assistant_id';
|
||||
$query = Db::name('doctor_appointment')
|
||||
@@ -662,7 +923,7 @@ class ConversionLogic
|
||||
|
||||
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
|
||||
|
||||
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds);
|
||||
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds, $visibleAdminIds);
|
||||
if ($mappedEntityIds === []) {
|
||||
continue;
|
||||
}
|
||||
@@ -675,7 +936,7 @@ class ConversionLogic
|
||||
}
|
||||
}
|
||||
|
||||
self::hydratePaidAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
|
||||
self::hydratePaidAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
|
||||
|
||||
foreach ($entities as &$entity) {
|
||||
$appointmentTotalCount = (int)($entity['appointment_total_count'] ?? 0);
|
||||
@@ -690,6 +951,7 @@ class ConversionLogic
|
||||
* @param int[] $entityIds
|
||||
* @param array<int, int[]> $adminToDeptIds
|
||||
* @param array<string, mixed>|null $mediaChannel
|
||||
* @param int[]|null $visibleAdminIds
|
||||
*/
|
||||
private static function hydratePaidAppointmentStats(
|
||||
array &$entities,
|
||||
@@ -698,7 +960,8 @@ class ConversionLogic
|
||||
array $adminToDeptIds,
|
||||
int $startTimestamp,
|
||||
int $endTimestamp,
|
||||
?array $mediaChannel
|
||||
?array $mediaChannel,
|
||||
?array $visibleAdminIds = null
|
||||
): void {
|
||||
$startDateTime = date('Y-m-d H:i:s', $startTimestamp);
|
||||
$endDateTime = date('Y-m-d H:i:s', $endTimestamp);
|
||||
@@ -720,7 +983,7 @@ class ConversionLogic
|
||||
$rows = $query->select()->toArray();
|
||||
foreach ($rows as $row) {
|
||||
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
|
||||
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds);
|
||||
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds, $visibleAdminIds);
|
||||
if ($mappedEntityIds === []) {
|
||||
continue;
|
||||
}
|
||||
@@ -737,6 +1000,7 @@ class ConversionLogic
|
||||
* @param int[] $entityIds
|
||||
* @param array<int, int[]> $adminToDeptIds
|
||||
* @param array<string, mixed>|null $mediaChannel
|
||||
* @param int[]|null $visibleAdminIds
|
||||
*/
|
||||
private static function hydrateOrderAndAmountStats(
|
||||
array &$entities,
|
||||
@@ -745,7 +1009,8 @@ class ConversionLogic
|
||||
array $adminToDeptIds,
|
||||
int $startTimestamp,
|
||||
int $endTimestamp,
|
||||
?array $mediaChannel
|
||||
?array $mediaChannel,
|
||||
?array $visibleAdminIds = null
|
||||
): void {
|
||||
$completedSourceExpr = $dimension === 'doctor' ? 'rx.creator_id' : 'rx.assistant_id';
|
||||
$completedQuery = Db::name('tcm_prescription_order')
|
||||
@@ -770,7 +1035,7 @@ class ConversionLogic
|
||||
foreach ($completedRows as $row) {
|
||||
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
|
||||
|
||||
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds);
|
||||
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds, $visibleAdminIds);
|
||||
if ($mappedEntityIds === []) {
|
||||
continue;
|
||||
}
|
||||
@@ -805,7 +1070,7 @@ class ConversionLogic
|
||||
|
||||
foreach ($businessRows as $row) {
|
||||
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
|
||||
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds);
|
||||
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds, $visibleAdminIds);
|
||||
if ($mappedEntityIds === []) {
|
||||
continue;
|
||||
}
|
||||
@@ -821,9 +1086,16 @@ class ConversionLogic
|
||||
* 账户消耗:来源于独立维护表 zyt_account_cost。
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $entities
|
||||
* @param int[]|null $visibleDeptIds 可见部门集合(null = SCOPE_ALL,不收窄)
|
||||
* @return array{0: float, 1: int[]}
|
||||
*/
|
||||
private static function hydrateAccountCostStats(array &$entities, string $startDate, string $endDate, string $mediaChannelCode): array
|
||||
private static function hydrateAccountCostStats(
|
||||
array &$entities,
|
||||
string $startDate,
|
||||
string $endDate,
|
||||
string $mediaChannelCode,
|
||||
?array $visibleDeptIds = null
|
||||
): array
|
||||
{
|
||||
$supportsDeptBinding = AccountCost::supportsDeptBinding();
|
||||
$query = Db::name('account_cost')
|
||||
@@ -842,6 +1114,19 @@ class ConversionLogic
|
||||
|
||||
if ($supportsDeptBinding) {
|
||||
$query->where('dept_id', '>', 0);
|
||||
// 数据隔离:仅统计可见部门的账户消耗;不支持 dept_binding 时无法按部门收窄,跳过。
|
||||
if ($visibleDeptIds !== null) {
|
||||
if ($visibleDeptIds === []) {
|
||||
foreach ($entities as &$entity) {
|
||||
$entity['account_cost'] = 0.0;
|
||||
$entity['_global_account_cost'] = 0.0;
|
||||
}
|
||||
unset($entity);
|
||||
|
||||
return [0.0, []];
|
||||
}
|
||||
$query->whereIn('dept_id', $visibleDeptIds);
|
||||
}
|
||||
}
|
||||
|
||||
$rows = $query->select()->toArray();
|
||||
@@ -870,16 +1155,25 @@ class ConversionLogic
|
||||
/**
|
||||
* @param int[] $entityIds
|
||||
* @param array<int, int[]> $adminToDeptIds
|
||||
* @param int[]|null $visibleAdminIds 可见 admin 集合;非 null 时 admin 不在集合中直接拒绝(不归到任何 entity)
|
||||
* @return int[]
|
||||
*
|
||||
* dept 维度下 admin 跨部门时,仅返回 admin_dept 中第一个落在当前 entityIds 内的部门
|
||||
* (由 loadAdminDeptMap 排序决定),保证同一笔指标只累加到一个部门,避免重复计数。
|
||||
*/
|
||||
private static function mapEntityIds(string $dimension, int $adminId, array $entityIds, array $adminToDeptIds): array
|
||||
{
|
||||
private static function mapEntityIds(
|
||||
string $dimension,
|
||||
int $adminId,
|
||||
array $entityIds,
|
||||
array $adminToDeptIds,
|
||||
?array $visibleAdminIds = null
|
||||
): array {
|
||||
if ($adminId <= 0) {
|
||||
return [];
|
||||
}
|
||||
if ($visibleAdminIds !== null && !in_array($adminId, $visibleAdminIds, true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($dimension !== 'dept') {
|
||||
return in_array($adminId, $entityIds, true) ? [$adminId] : [];
|
||||
@@ -1228,6 +1522,7 @@ class ConversionLogic
|
||||
* 挂到不存在的部门上、错过"未分配部门"虚拟桶。
|
||||
* @param float $globalAccountCost 由调用方提前计算好的本期总账户消耗(zyt_account_cost SUM)。
|
||||
* -1 表示让本函数自行 hydrate;>= 0 时直接复用,避免重复 SQL。
|
||||
* @param int[]|null $visibleAdminIds 可见 admin 集合(null = SCOPE_ALL);用于成员明细的隔离
|
||||
* @return array<int, array<int, array<string, mixed>>> dept_id => [member_row, ...]
|
||||
*/
|
||||
private static function buildMemberRowsByDept(
|
||||
@@ -1241,24 +1536,25 @@ class ConversionLogic
|
||||
array $eligibleDeptIds,
|
||||
array $adminToDeptIds,
|
||||
array $validDeptIds = [],
|
||||
float $globalAccountCost = -1.0
|
||||
float $globalAccountCost = -1.0,
|
||||
?array $visibleAdminIds = null
|
||||
): array
|
||||
{
|
||||
$assistantEntities = self::loadAdminEntities(2);
|
||||
$doctorEntities = self::loadAdminEntities(1);
|
||||
$assistantEntities = self::loadAdminEntities(2, 0, $visibleAdminIds);
|
||||
$doctorEntities = self::loadAdminEntities(1, 0, $visibleAdminIds);
|
||||
|
||||
$assistantIds = array_keys($assistantEntities);
|
||||
$doctorIds = array_keys($doctorEntities);
|
||||
|
||||
if ($assistantIds !== []) {
|
||||
self::hydrateFanStats($assistantEntities, 'assistant', $assistantIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
|
||||
self::hydrateAppointmentStats($assistantEntities, 'assistant', $assistantIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel);
|
||||
self::hydrateOrderAndAmountStats($assistantEntities, 'assistant', $assistantIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
|
||||
self::hydrateFanStats($assistantEntities, 'assistant', $assistantIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
|
||||
self::hydrateAppointmentStats($assistantEntities, 'assistant', $assistantIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel, $visibleAdminIds);
|
||||
self::hydrateOrderAndAmountStats($assistantEntities, 'assistant', $assistantIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
|
||||
}
|
||||
if ($doctorIds !== []) {
|
||||
self::hydrateFanStats($doctorEntities, 'doctor', $doctorIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
|
||||
self::hydrateAppointmentStats($doctorEntities, 'doctor', $doctorIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel);
|
||||
self::hydrateOrderAndAmountStats($doctorEntities, 'doctor', $doctorIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
|
||||
self::hydrateFanStats($doctorEntities, 'doctor', $doctorIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
|
||||
self::hydrateAppointmentStats($doctorEntities, 'doctor', $doctorIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel, $visibleAdminIds);
|
||||
self::hydrateOrderAndAmountStats($doctorEntities, 'doctor', $doctorIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
|
||||
}
|
||||
|
||||
// 复用调用方传入的 global account cost;只有兜底未传时才回查一次(保留向后兼容)。
|
||||
@@ -1366,9 +1662,12 @@ class ConversionLogic
|
||||
$memberRowsByDeptId[$primaryDeptId][] = $row;
|
||||
}
|
||||
|
||||
$unboundRows = self::buildUnboundFansRows($startTimestamp, $endTimestamp, $mediaChannel);
|
||||
if ($unboundRows !== []) {
|
||||
$memberRowsByDeptId[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID] = $unboundRows;
|
||||
// -1 桶仅在 SCOPE_ALL 下展示,因此非 SCOPE_ALL 时不再查未绑定 admin 的加粉。
|
||||
if ($visibleAdminIds === null) {
|
||||
$unboundRows = self::buildUnboundFansRows($startTimestamp, $endTimestamp, $mediaChannel);
|
||||
if ($unboundRows !== []) {
|
||||
$memberRowsByDeptId[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID] = $unboundRows;
|
||||
}
|
||||
}
|
||||
|
||||
// 补充:dept 维度 hydrate 把任意角色的 admin 加粉都归到 -2 桶,
|
||||
@@ -1379,7 +1678,8 @@ class ConversionLogic
|
||||
$startTimestamp,
|
||||
$endTimestamp,
|
||||
$mediaChannel,
|
||||
$assignedAdminIds
|
||||
$assignedAdminIds,
|
||||
$visibleAdminIds
|
||||
);
|
||||
if ($leftoverRows !== []) {
|
||||
$existing = $memberRowsByDeptId[self::VIRTUAL_DEPT_UNASSIGNED_ID] ?? [];
|
||||
@@ -1477,13 +1777,15 @@ class ConversionLogic
|
||||
* 这些 admin 的业务指标无法从医助/医生口径得出,因此仅展示加粉数。
|
||||
*
|
||||
* @param array<int, true> $assignedAdminIds 已挂过部门或 -2 桶的 admin_id 集合
|
||||
* @param int[]|null $visibleAdminIds 可见 admin 集合(null = SCOPE_ALL,不收窄)
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function buildLeftoverAdminRowsForUnassigned(
|
||||
int $startTimestamp,
|
||||
int $endTimestamp,
|
||||
?array $mediaChannel,
|
||||
array $assignedAdminIds
|
||||
array $assignedAdminIds,
|
||||
?array $visibleAdminIds = null
|
||||
): array
|
||||
{
|
||||
$query = Db::name('qywx_external_contact_event')
|
||||
@@ -1494,6 +1796,13 @@ class ConversionLogic
|
||||
->fieldRaw('a.id AS admin_id, a.name AS admin_name, COUNT(*) AS add_fans_count')
|
||||
->group('a.id, a.name');
|
||||
|
||||
if ($visibleAdminIds !== null) {
|
||||
if ($visibleAdminIds === []) {
|
||||
return [];
|
||||
}
|
||||
$query->whereIn('a.id', $visibleAdminIds);
|
||||
}
|
||||
|
||||
if ($mediaChannel !== null) {
|
||||
$query->leftJoin('qywx_external_contact q', 'q.external_userid = e.external_userid');
|
||||
MediaChannelService::applyFollowUsersChannelFilter($query, 'q.follow_users', $mediaChannel);
|
||||
|
||||
Reference in New Issue
Block a user