增加数据权限
增加调试登录
This commit is contained in:
@@ -11,7 +11,7 @@ class ConversionController extends BaseAdminController
|
|||||||
{
|
{
|
||||||
public function overview()
|
public function overview()
|
||||||
{
|
{
|
||||||
$result = ConversionLogic::overview($this->request->get());
|
$result = ConversionLogic::overview($this->request->get(), $this->adminId, $this->adminInfo);
|
||||||
|
|
||||||
return $this->data($result);
|
return $this->data($result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace app\adminapi\logic\stats;
|
|||||||
use app\adminapi\logic\dept\DeptLogic;
|
use app\adminapi\logic\dept\DeptLogic;
|
||||||
use app\adminapi\logic\tcm\DiagnosisLogic;
|
use app\adminapi\logic\tcm\DiagnosisLogic;
|
||||||
use app\common\model\finance\AccountCost;
|
use app\common\model\finance\AccountCost;
|
||||||
|
use app\common\service\DataScope\DataScopeService;
|
||||||
use app\common\service\qywx\MediaChannelService;
|
use app\common\service\qywx\MediaChannelService;
|
||||||
use think\facade\Db;
|
use think\facade\Db;
|
||||||
|
|
||||||
@@ -16,9 +17,17 @@ class ConversionLogic
|
|||||||
private const VIRTUAL_DEPT_UNASSIGNED_ID = -2;
|
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>
|
* @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;
|
$includeFilters = (int)($params['include_filters'] ?? 0) === 1;
|
||||||
$dimension = self::normalizeDimension((string)($params['dimension'] ?? 'dept'));
|
$dimension = self::normalizeDimension((string)($params['dimension'] ?? 'dept'));
|
||||||
@@ -29,9 +38,37 @@ class ConversionLogic
|
|||||||
$pageNo = max(1, (int)($params['page_no'] ?? 1));
|
$pageNo = max(1, (int)($params['page_no'] ?? 1));
|
||||||
$pageSize = max(1, min(100, (int)($params['page_size'] ?? 15)));
|
$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);
|
$entityIds = array_keys($entities);
|
||||||
$allocationEntities = self::loadEntities($dimension, $params);
|
$allocationEntities = self::loadEntities($dimension, $params, $visibleAdminIds);
|
||||||
$allocationEntityIds = array_keys($allocationEntities);
|
$allocationEntityIds = array_keys($allocationEntities);
|
||||||
|
|
||||||
if ($entityIds === []) {
|
if ($entityIds === []) {
|
||||||
@@ -52,18 +89,20 @@ class ConversionLogic
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
if ($includeFilters) {
|
if ($includeFilters) {
|
||||||
$result['extend']['filters'] = self::buildFilterOptions();
|
$result['extend']['filters'] = self::buildFilterOptions($visibleAdminIds, [], $adminId, $adminInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
$adminToDeptIds = self::loadAdminDeptMap();
|
$adminToDeptIds = self::loadAdminDeptMap();
|
||||||
self::hydrateFanStats($allocationEntities, $dimension, $allocationEntityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
|
self::hydrateFanStats($allocationEntities, $dimension, $allocationEntityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
|
||||||
self::hydrateFanStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
|
self::hydrateFanStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
|
||||||
self::hydrateAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel);
|
self::hydrateAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel, $visibleAdminIds);
|
||||||
self::hydrateOrderAndAmountStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
|
self::hydrateOrderAndAmountStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
|
||||||
[$globalAccountCost, $accountCostDeptIds] = self::hydrateAccountCostStats($entities, $startDate, $endDate, $mediaChannelCode);
|
// 数据隔离:可见部门 = 可见 admin 所属部门并集;用于 account_cost 与下游 cost 分摊。
|
||||||
|
$visibleDeptIds = self::resolveVisibleDeptIds($visibleAdminIds);
|
||||||
|
[$globalAccountCost, $accountCostDeptIds] = self::hydrateAccountCostStats($entities, $startDate, $endDate, $mediaChannelCode, $visibleDeptIds);
|
||||||
$supportsDeptBinding = AccountCost::supportsDeptBinding();
|
$supportsDeptBinding = AccountCost::supportsDeptBinding();
|
||||||
$restrictAccountCostByDept = $supportsDeptBinding;
|
$restrictAccountCostByDept = $supportsDeptBinding;
|
||||||
$restrictStatsByDept = $supportsDeptBinding && $mediaChannelCode !== '';
|
$restrictStatsByDept = $supportsDeptBinding && $mediaChannelCode !== '';
|
||||||
@@ -98,7 +137,7 @@ class ConversionLogic
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
if ($includeFilters) {
|
if ($includeFilters) {
|
||||||
$result['extend']['filters'] = self::buildFilterOptions();
|
$result['extend']['filters'] = self::buildFilterOptions($visibleAdminIds, $eligibleDeptIds, $adminId, $adminInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
@@ -142,7 +181,8 @@ class ConversionLogic
|
|||||||
$eligibleDeptIds,
|
$eligibleDeptIds,
|
||||||
$adminToDeptIds,
|
$adminToDeptIds,
|
||||||
$validDeptIds,
|
$validDeptIds,
|
||||||
$globalAccountCost
|
$globalAccountCost,
|
||||||
|
$visibleAdminIds
|
||||||
);
|
);
|
||||||
$pagedRows = self::attachDeptMembers($pagedRows, $memberRowsByDeptId);
|
$pagedRows = self::attachDeptMembers($pagedRows, $memberRowsByDeptId);
|
||||||
}
|
}
|
||||||
@@ -164,7 +204,7 @@ class ConversionLogic
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
if ($includeFilters) {
|
if ($includeFilters) {
|
||||||
$result['extend']['filters'] = self::buildFilterOptions();
|
$result['extend']['filters'] = self::buildFilterOptions($visibleAdminIds, $eligibleDeptIds, $adminId, $adminInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
@@ -199,25 +239,194 @@ class ConversionLogic
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
if ($includeFilters) {
|
if ($includeFilters) {
|
||||||
$result['extend']['filters'] = self::buildFilterOptions();
|
$result['extend']['filters'] = self::buildFilterOptions($visibleAdminIds, $eligibleDeptIds, $adminId, $adminInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param int[]|null $visibleAdminIds 可见 admin 集合(null = SCOPE_ALL)
|
||||||
|
* @param int[] $eligibleDeptIds 当媒体渠道筛选生效时收窄过的部门作用域
|
||||||
* @return array<string, mixed>
|
* @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 [
|
return [
|
||||||
'departments' => DeptLogic::getAllData(),
|
'departments' => self::buildDepartmentOptions($visibleAdminIds, $eligibleDeptIds),
|
||||||
'assistants' => DiagnosisLogic::getAssistants(),
|
'assistants' => DiagnosisLogic::getAssistants($adminId, $adminInfo),
|
||||||
'doctors' => DiagnosisLogic::getDoctors(),
|
'doctors' => self::buildDoctorOptions($visibleAdminIds),
|
||||||
'media_channels' => MediaChannelService::getOptions(),
|
'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
|
private static function normalizeDimension(string $dimension): string
|
||||||
{
|
{
|
||||||
return in_array($dimension, ['dept', 'assistant', 'doctor'], true) ? $dimension : 'dept';
|
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>>
|
* @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) {
|
$entities = match ($dimension) {
|
||||||
'assistant' => self::loadAdminEntities(2, isset($params['assistant_id']) ? (int)$params['assistant_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),
|
'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),
|
default => self::loadDeptEntities(isset($params['dept_id']) ? (int)$params['dept_id'] : 0),
|
||||||
};
|
};
|
||||||
|
|
||||||
if ($dimension === 'dept' && (int)($params['dept_id'] ?? 0) <= 0) {
|
if ($dimension === 'dept' && (int)($params['dept_id'] ?? 0) <= 0) {
|
||||||
self::appendVirtualDeptEntities($entities);
|
self::appendVirtualDeptEntities($entities, $visibleAdminIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $entities;
|
return $entities;
|
||||||
@@ -348,9 +558,10 @@ class ConversionLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param int[]|null $visibleAdminIds 可见 admin 集合(null = SCOPE_ALL,不收窄)
|
||||||
* @return array<int, array<string, mixed>>
|
* @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')
|
$query = Db::name('admin')
|
||||||
->alias('a')
|
->alias('a')
|
||||||
@@ -362,6 +573,12 @@ class ConversionLogic
|
|||||||
if ($adminId > 0) {
|
if ($adminId > 0) {
|
||||||
$query->where('a.id', $adminId);
|
$query->where('a.id', $adminId);
|
||||||
}
|
}
|
||||||
|
if ($visibleAdminIds !== null) {
|
||||||
|
if ($visibleAdminIds === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$query->whereIn('a.id', $visibleAdminIds);
|
||||||
|
}
|
||||||
|
|
||||||
$rows = $query->order('a.id asc')->select()->toArray();
|
$rows = $query->order('a.id asc')->select()->toArray();
|
||||||
$entities = [];
|
$entities = [];
|
||||||
@@ -406,17 +623,21 @@ class ConversionLogic
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, array<string, mixed>> $entities
|
* @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(
|
if ($visibleAdminIds === null) {
|
||||||
self::newEntityRow(self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID, '未绑定后台账号'),
|
$entities[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID] = array_merge(
|
||||||
[
|
self::newEntityRow(self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID, '未绑定后台账号'),
|
||||||
'pid' => 0,
|
[
|
||||||
'sort' => -999998,
|
'pid' => 0,
|
||||||
'_virtual_bucket' => true,
|
'sort' => -999998,
|
||||||
]
|
'_virtual_bucket' => true,
|
||||||
);
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$entities[self::VIRTUAL_DEPT_UNASSIGNED_ID] = array_merge(
|
$entities[self::VIRTUAL_DEPT_UNASSIGNED_ID] = array_merge(
|
||||||
self::newEntityRow(self::VIRTUAL_DEPT_UNASSIGNED_ID, '未分配部门'),
|
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[]>
|
* @return array<int, int[]>
|
||||||
*
|
*
|
||||||
@@ -557,6 +809,7 @@ class ConversionLogic
|
|||||||
* @param int[] $entityIds
|
* @param int[] $entityIds
|
||||||
* @param array<int, int[]> $adminToDeptIds
|
* @param array<int, int[]> $adminToDeptIds
|
||||||
* @param array<string, mixed>|null $mediaChannel
|
* @param array<string, mixed>|null $mediaChannel
|
||||||
|
* @param int[]|null $visibleAdminIds 可见 admin 集合
|
||||||
*/
|
*/
|
||||||
private static function hydrateFanStats(
|
private static function hydrateFanStats(
|
||||||
array &$entities,
|
array &$entities,
|
||||||
@@ -565,7 +818,8 @@ class ConversionLogic
|
|||||||
array $adminToDeptIds,
|
array $adminToDeptIds,
|
||||||
int $startTimestamp,
|
int $startTimestamp,
|
||||||
int $endTimestamp,
|
int $endTimestamp,
|
||||||
?array $mediaChannel
|
?array $mediaChannel,
|
||||||
|
?array $visibleAdminIds = null
|
||||||
): void {
|
): void {
|
||||||
$query = Db::name('qywx_external_contact_event')
|
$query = Db::name('qywx_external_contact_event')
|
||||||
->alias('e')
|
->alias('e')
|
||||||
@@ -587,7 +841,8 @@ class ConversionLogic
|
|||||||
$addFansCount = (int)($row['add_fans_count'] ?? 0);
|
$addFansCount = (int)($row['add_fans_count'] ?? 0);
|
||||||
|
|
||||||
if ($dimension === 'dept' && $adminId <= 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;
|
$entities[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID]['add_fans_count'] += $addFansCount;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -597,8 +852,12 @@ class ConversionLogic
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$hitEntityIds = self::mapEntityIds($dimension, $adminId, $entityIds, $adminToDeptIds);
|
$hitEntityIds = self::mapEntityIds($dimension, $adminId, $entityIds, $adminToDeptIds, $visibleAdminIds);
|
||||||
if ($dimension === 'dept' && $hitEntityIds === []) {
|
if ($dimension === 'dept' && $hitEntityIds === []) {
|
||||||
|
// admin 已通过 visibleAdminIds 校验但没找到归属部门 → 落入"未分配部门"。
|
||||||
|
if ($visibleAdminIds !== null && !in_array($adminId, $visibleAdminIds, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (isset($entities[self::VIRTUAL_DEPT_UNASSIGNED_ID])) {
|
if (isset($entities[self::VIRTUAL_DEPT_UNASSIGNED_ID])) {
|
||||||
$entities[self::VIRTUAL_DEPT_UNASSIGNED_ID]['add_fans_count'] += $addFansCount;
|
$entities[self::VIRTUAL_DEPT_UNASSIGNED_ID]['add_fans_count'] += $addFansCount;
|
||||||
}
|
}
|
||||||
@@ -620,6 +879,7 @@ class ConversionLogic
|
|||||||
* @param int[] $entityIds
|
* @param int[] $entityIds
|
||||||
* @param array<int, int[]> $adminToDeptIds
|
* @param array<int, int[]> $adminToDeptIds
|
||||||
* @param array<string, mixed>|null $mediaChannel
|
* @param array<string, mixed>|null $mediaChannel
|
||||||
|
* @param int[]|null $visibleAdminIds
|
||||||
*/
|
*/
|
||||||
private static function hydrateAppointmentStats(
|
private static function hydrateAppointmentStats(
|
||||||
array &$entities,
|
array &$entities,
|
||||||
@@ -630,7 +890,8 @@ class ConversionLogic
|
|||||||
int $endTimestamp,
|
int $endTimestamp,
|
||||||
string $startDate,
|
string $startDate,
|
||||||
string $endDate,
|
string $endDate,
|
||||||
?array $mediaChannel
|
?array $mediaChannel,
|
||||||
|
?array $visibleAdminIds = null
|
||||||
): void {
|
): void {
|
||||||
$sourceExpr = $dimension === 'doctor' ? 'a.doctor_id' : 'u.assistant_id';
|
$sourceExpr = $dimension === 'doctor' ? 'a.doctor_id' : 'u.assistant_id';
|
||||||
$query = Db::name('doctor_appointment')
|
$query = Db::name('doctor_appointment')
|
||||||
@@ -662,7 +923,7 @@ class ConversionLogic
|
|||||||
|
|
||||||
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
|
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
|
||||||
|
|
||||||
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds);
|
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds, $visibleAdminIds);
|
||||||
if ($mappedEntityIds === []) {
|
if ($mappedEntityIds === []) {
|
||||||
continue;
|
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) {
|
foreach ($entities as &$entity) {
|
||||||
$appointmentTotalCount = (int)($entity['appointment_total_count'] ?? 0);
|
$appointmentTotalCount = (int)($entity['appointment_total_count'] ?? 0);
|
||||||
@@ -690,6 +951,7 @@ class ConversionLogic
|
|||||||
* @param int[] $entityIds
|
* @param int[] $entityIds
|
||||||
* @param array<int, int[]> $adminToDeptIds
|
* @param array<int, int[]> $adminToDeptIds
|
||||||
* @param array<string, mixed>|null $mediaChannel
|
* @param array<string, mixed>|null $mediaChannel
|
||||||
|
* @param int[]|null $visibleAdminIds
|
||||||
*/
|
*/
|
||||||
private static function hydratePaidAppointmentStats(
|
private static function hydratePaidAppointmentStats(
|
||||||
array &$entities,
|
array &$entities,
|
||||||
@@ -698,7 +960,8 @@ class ConversionLogic
|
|||||||
array $adminToDeptIds,
|
array $adminToDeptIds,
|
||||||
int $startTimestamp,
|
int $startTimestamp,
|
||||||
int $endTimestamp,
|
int $endTimestamp,
|
||||||
?array $mediaChannel
|
?array $mediaChannel,
|
||||||
|
?array $visibleAdminIds = null
|
||||||
): void {
|
): void {
|
||||||
$startDateTime = date('Y-m-d H:i:s', $startTimestamp);
|
$startDateTime = date('Y-m-d H:i:s', $startTimestamp);
|
||||||
$endDateTime = date('Y-m-d H:i:s', $endTimestamp);
|
$endDateTime = date('Y-m-d H:i:s', $endTimestamp);
|
||||||
@@ -720,7 +983,7 @@ class ConversionLogic
|
|||||||
$rows = $query->select()->toArray();
|
$rows = $query->select()->toArray();
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
|
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
|
||||||
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds);
|
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds, $visibleAdminIds);
|
||||||
if ($mappedEntityIds === []) {
|
if ($mappedEntityIds === []) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -737,6 +1000,7 @@ class ConversionLogic
|
|||||||
* @param int[] $entityIds
|
* @param int[] $entityIds
|
||||||
* @param array<int, int[]> $adminToDeptIds
|
* @param array<int, int[]> $adminToDeptIds
|
||||||
* @param array<string, mixed>|null $mediaChannel
|
* @param array<string, mixed>|null $mediaChannel
|
||||||
|
* @param int[]|null $visibleAdminIds
|
||||||
*/
|
*/
|
||||||
private static function hydrateOrderAndAmountStats(
|
private static function hydrateOrderAndAmountStats(
|
||||||
array &$entities,
|
array &$entities,
|
||||||
@@ -745,7 +1009,8 @@ class ConversionLogic
|
|||||||
array $adminToDeptIds,
|
array $adminToDeptIds,
|
||||||
int $startTimestamp,
|
int $startTimestamp,
|
||||||
int $endTimestamp,
|
int $endTimestamp,
|
||||||
?array $mediaChannel
|
?array $mediaChannel,
|
||||||
|
?array $visibleAdminIds = null
|
||||||
): void {
|
): void {
|
||||||
$completedSourceExpr = $dimension === 'doctor' ? 'rx.creator_id' : 'rx.assistant_id';
|
$completedSourceExpr = $dimension === 'doctor' ? 'rx.creator_id' : 'rx.assistant_id';
|
||||||
$completedQuery = Db::name('tcm_prescription_order')
|
$completedQuery = Db::name('tcm_prescription_order')
|
||||||
@@ -770,7 +1035,7 @@ class ConversionLogic
|
|||||||
foreach ($completedRows as $row) {
|
foreach ($completedRows as $row) {
|
||||||
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
|
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
|
||||||
|
|
||||||
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds);
|
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds, $visibleAdminIds);
|
||||||
if ($mappedEntityIds === []) {
|
if ($mappedEntityIds === []) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -805,7 +1070,7 @@ class ConversionLogic
|
|||||||
|
|
||||||
foreach ($businessRows as $row) {
|
foreach ($businessRows as $row) {
|
||||||
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
|
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
|
||||||
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds);
|
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds, $visibleAdminIds);
|
||||||
if ($mappedEntityIds === []) {
|
if ($mappedEntityIds === []) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -821,9 +1086,16 @@ class ConversionLogic
|
|||||||
* 账户消耗:来源于独立维护表 zyt_account_cost。
|
* 账户消耗:来源于独立维护表 zyt_account_cost。
|
||||||
*
|
*
|
||||||
* @param array<int, array<string, mixed>> $entities
|
* @param array<int, array<string, mixed>> $entities
|
||||||
|
* @param int[]|null $visibleDeptIds 可见部门集合(null = SCOPE_ALL,不收窄)
|
||||||
* @return array{0: float, 1: int[]}
|
* @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();
|
$supportsDeptBinding = AccountCost::supportsDeptBinding();
|
||||||
$query = Db::name('account_cost')
|
$query = Db::name('account_cost')
|
||||||
@@ -842,6 +1114,19 @@ class ConversionLogic
|
|||||||
|
|
||||||
if ($supportsDeptBinding) {
|
if ($supportsDeptBinding) {
|
||||||
$query->where('dept_id', '>', 0);
|
$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();
|
$rows = $query->select()->toArray();
|
||||||
@@ -870,16 +1155,25 @@ class ConversionLogic
|
|||||||
/**
|
/**
|
||||||
* @param int[] $entityIds
|
* @param int[] $entityIds
|
||||||
* @param array<int, int[]> $adminToDeptIds
|
* @param array<int, int[]> $adminToDeptIds
|
||||||
|
* @param int[]|null $visibleAdminIds 可见 admin 集合;非 null 时 admin 不在集合中直接拒绝(不归到任何 entity)
|
||||||
* @return int[]
|
* @return int[]
|
||||||
*
|
*
|
||||||
* dept 维度下 admin 跨部门时,仅返回 admin_dept 中第一个落在当前 entityIds 内的部门
|
* dept 维度下 admin 跨部门时,仅返回 admin_dept 中第一个落在当前 entityIds 内的部门
|
||||||
* (由 loadAdminDeptMap 排序决定),保证同一笔指标只累加到一个部门,避免重复计数。
|
* (由 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) {
|
if ($adminId <= 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
if ($visibleAdminIds !== null && !in_array($adminId, $visibleAdminIds, true)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
if ($dimension !== 'dept') {
|
if ($dimension !== 'dept') {
|
||||||
return in_array($adminId, $entityIds, true) ? [$adminId] : [];
|
return in_array($adminId, $entityIds, true) ? [$adminId] : [];
|
||||||
@@ -1228,6 +1522,7 @@ class ConversionLogic
|
|||||||
* 挂到不存在的部门上、错过"未分配部门"虚拟桶。
|
* 挂到不存在的部门上、错过"未分配部门"虚拟桶。
|
||||||
* @param float $globalAccountCost 由调用方提前计算好的本期总账户消耗(zyt_account_cost SUM)。
|
* @param float $globalAccountCost 由调用方提前计算好的本期总账户消耗(zyt_account_cost SUM)。
|
||||||
* -1 表示让本函数自行 hydrate;>= 0 时直接复用,避免重复 SQL。
|
* -1 表示让本函数自行 hydrate;>= 0 时直接复用,避免重复 SQL。
|
||||||
|
* @param int[]|null $visibleAdminIds 可见 admin 集合(null = SCOPE_ALL);用于成员明细的隔离
|
||||||
* @return array<int, array<int, array<string, mixed>>> dept_id => [member_row, ...]
|
* @return array<int, array<int, array<string, mixed>>> dept_id => [member_row, ...]
|
||||||
*/
|
*/
|
||||||
private static function buildMemberRowsByDept(
|
private static function buildMemberRowsByDept(
|
||||||
@@ -1241,24 +1536,25 @@ class ConversionLogic
|
|||||||
array $eligibleDeptIds,
|
array $eligibleDeptIds,
|
||||||
array $adminToDeptIds,
|
array $adminToDeptIds,
|
||||||
array $validDeptIds = [],
|
array $validDeptIds = [],
|
||||||
float $globalAccountCost = -1.0
|
float $globalAccountCost = -1.0,
|
||||||
|
?array $visibleAdminIds = null
|
||||||
): array
|
): array
|
||||||
{
|
{
|
||||||
$assistantEntities = self::loadAdminEntities(2);
|
$assistantEntities = self::loadAdminEntities(2, 0, $visibleAdminIds);
|
||||||
$doctorEntities = self::loadAdminEntities(1);
|
$doctorEntities = self::loadAdminEntities(1, 0, $visibleAdminIds);
|
||||||
|
|
||||||
$assistantIds = array_keys($assistantEntities);
|
$assistantIds = array_keys($assistantEntities);
|
||||||
$doctorIds = array_keys($doctorEntities);
|
$doctorIds = array_keys($doctorEntities);
|
||||||
|
|
||||||
if ($assistantIds !== []) {
|
if ($assistantIds !== []) {
|
||||||
self::hydrateFanStats($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);
|
self::hydrateAppointmentStats($assistantEntities, 'assistant', $assistantIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel, $visibleAdminIds);
|
||||||
self::hydrateOrderAndAmountStats($assistantEntities, 'assistant', $assistantIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
|
self::hydrateOrderAndAmountStats($assistantEntities, 'assistant', $assistantIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
|
||||||
}
|
}
|
||||||
if ($doctorIds !== []) {
|
if ($doctorIds !== []) {
|
||||||
self::hydrateFanStats($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);
|
self::hydrateAppointmentStats($doctorEntities, 'doctor', $doctorIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel, $visibleAdminIds);
|
||||||
self::hydrateOrderAndAmountStats($doctorEntities, 'doctor', $doctorIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
|
self::hydrateOrderAndAmountStats($doctorEntities, 'doctor', $doctorIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复用调用方传入的 global account cost;只有兜底未传时才回查一次(保留向后兼容)。
|
// 复用调用方传入的 global account cost;只有兜底未传时才回查一次(保留向后兼容)。
|
||||||
@@ -1366,9 +1662,12 @@ class ConversionLogic
|
|||||||
$memberRowsByDeptId[$primaryDeptId][] = $row;
|
$memberRowsByDeptId[$primaryDeptId][] = $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
$unboundRows = self::buildUnboundFansRows($startTimestamp, $endTimestamp, $mediaChannel);
|
// -1 桶仅在 SCOPE_ALL 下展示,因此非 SCOPE_ALL 时不再查未绑定 admin 的加粉。
|
||||||
if ($unboundRows !== []) {
|
if ($visibleAdminIds === null) {
|
||||||
$memberRowsByDeptId[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID] = $unboundRows;
|
$unboundRows = self::buildUnboundFansRows($startTimestamp, $endTimestamp, $mediaChannel);
|
||||||
|
if ($unboundRows !== []) {
|
||||||
|
$memberRowsByDeptId[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID] = $unboundRows;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 补充:dept 维度 hydrate 把任意角色的 admin 加粉都归到 -2 桶,
|
// 补充:dept 维度 hydrate 把任意角色的 admin 加粉都归到 -2 桶,
|
||||||
@@ -1379,7 +1678,8 @@ class ConversionLogic
|
|||||||
$startTimestamp,
|
$startTimestamp,
|
||||||
$endTimestamp,
|
$endTimestamp,
|
||||||
$mediaChannel,
|
$mediaChannel,
|
||||||
$assignedAdminIds
|
$assignedAdminIds,
|
||||||
|
$visibleAdminIds
|
||||||
);
|
);
|
||||||
if ($leftoverRows !== []) {
|
if ($leftoverRows !== []) {
|
||||||
$existing = $memberRowsByDeptId[self::VIRTUAL_DEPT_UNASSIGNED_ID] ?? [];
|
$existing = $memberRowsByDeptId[self::VIRTUAL_DEPT_UNASSIGNED_ID] ?? [];
|
||||||
@@ -1477,13 +1777,15 @@ class ConversionLogic
|
|||||||
* 这些 admin 的业务指标无法从医助/医生口径得出,因此仅展示加粉数。
|
* 这些 admin 的业务指标无法从医助/医生口径得出,因此仅展示加粉数。
|
||||||
*
|
*
|
||||||
* @param array<int, true> $assignedAdminIds 已挂过部门或 -2 桶的 admin_id 集合
|
* @param array<int, true> $assignedAdminIds 已挂过部门或 -2 桶的 admin_id 集合
|
||||||
|
* @param int[]|null $visibleAdminIds 可见 admin 集合(null = SCOPE_ALL,不收窄)
|
||||||
* @return array<int, array<string, mixed>>
|
* @return array<int, array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
private static function buildLeftoverAdminRowsForUnassigned(
|
private static function buildLeftoverAdminRowsForUnassigned(
|
||||||
int $startTimestamp,
|
int $startTimestamp,
|
||||||
int $endTimestamp,
|
int $endTimestamp,
|
||||||
?array $mediaChannel,
|
?array $mediaChannel,
|
||||||
array $assignedAdminIds
|
array $assignedAdminIds,
|
||||||
|
?array $visibleAdminIds = null
|
||||||
): array
|
): array
|
||||||
{
|
{
|
||||||
$query = Db::name('qywx_external_contact_event')
|
$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')
|
->fieldRaw('a.id AS admin_id, a.name AS admin_name, COUNT(*) AS add_fans_count')
|
||||||
->group('a.id, a.name');
|
->group('a.id, a.name');
|
||||||
|
|
||||||
|
if ($visibleAdminIds !== null) {
|
||||||
|
if ($visibleAdminIds === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$query->whereIn('a.id', $visibleAdminIds);
|
||||||
|
}
|
||||||
|
|
||||||
if ($mediaChannel !== null) {
|
if ($mediaChannel !== null) {
|
||||||
$query->leftJoin('qywx_external_contact q', 'q.external_userid = e.external_userid');
|
$query->leftJoin('qywx_external_contact q', 'q.external_userid = e.external_userid');
|
||||||
MediaChannelService::applyFollowUsersChannelFilter($query, 'q.follow_users', $mediaChannel);
|
MediaChannelService::applyFollowUsersChannelFilter($query, 'q.follow_users', $mediaChannel);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use app\common\cache\AdminAccountSafeCache;
|
|||||||
use app\common\service\ConfigService;
|
use app\common\service\ConfigService;
|
||||||
use app\common\validate\BaseValidate;
|
use app\common\validate\BaseValidate;
|
||||||
use think\facade\Config;
|
use think\facade\Config;
|
||||||
|
use think\facade\Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录验证
|
* 登录验证
|
||||||
@@ -55,6 +56,27 @@ class LoginValidate extends BaseValidate
|
|||||||
*/
|
*/
|
||||||
public function password($password, $other, $data)
|
public function password($password, $other, $data)
|
||||||
{
|
{
|
||||||
|
// 本地调试:免密登录开关。须同时满足 APP_DEBUG=1 + LOCAL_DEBUG.LOGIN_SKIP_PASSWORD=true,
|
||||||
|
// 任一不满足都走原始密码校验。每次免密登录都会记一条 warning 日志,便于发现误开。
|
||||||
|
if (self::isLocalDebugLoginBypassEnabled()) {
|
||||||
|
$adminInfo = Admin::where('account', '=', $data['account'])
|
||||||
|
->field(['password,disable'])
|
||||||
|
->findOrEmpty();
|
||||||
|
if ($adminInfo->isEmpty()) {
|
||||||
|
return '账号不存在';
|
||||||
|
}
|
||||||
|
if ((int)($adminInfo['disable'] ?? 0) === 1) {
|
||||||
|
return '账号已禁用';
|
||||||
|
}
|
||||||
|
Log::warning(sprintf(
|
||||||
|
'[LOCAL_DEBUG] login bypass password: account=%s, ip=%s',
|
||||||
|
(string)($data['account'] ?? ''),
|
||||||
|
(string)request()->ip()
|
||||||
|
));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 登录限制
|
// 登录限制
|
||||||
$config = [
|
$config = [
|
||||||
'login_restrictions' => ConfigService::get('admin_login', 'login_restrictions'),
|
'login_restrictions' => ConfigService::get('admin_login', 'login_restrictions'),
|
||||||
@@ -100,4 +122,25 @@ class LoginValidate extends BaseValidate
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用"本地调试免密登录"。
|
||||||
|
*
|
||||||
|
* 双重闸门:
|
||||||
|
* 1. APP_DEBUG 必须为 true(即应用本身处于调试模式)
|
||||||
|
* 2. LOCAL_DEBUG.LOGIN_SKIP_PASSWORD 必须显式为 true / 1 / yes / on
|
||||||
|
*
|
||||||
|
* 任何一条不满足都返回 false,强制走原始密码校验。这样即使误把
|
||||||
|
* LOGIN_SKIP_PASSWORD 配进生产,只要 APP_DEBUG=0 仍然安全。
|
||||||
|
*/
|
||||||
|
private static function isLocalDebugLoginBypassEnabled(): bool
|
||||||
|
{
|
||||||
|
$appDebug = strtolower((string) env('app_debug', '0'));
|
||||||
|
if (!in_array($appDebug, ['1', 'true', 'yes', 'on'], true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$flag = strtolower((string) env('local_debug.login_skip_password', ''));
|
||||||
|
|
||||||
|
return in_array($flag, ['1', 'true', 'yes', 'on'], true);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user