fix(stats/self_input): 渠道字典、财务脱敏与同日同渠道全局唯一

自媒体来源改推广渠道字典;非白名单隐藏账户消耗/ROI;部门展示完整路径;业绩与消耗按日期+渠道全局去重。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-22 12:02:12 +08:00
co-authored by Cursor
parent bba989c0af
commit e4f181e4fe
13 changed files with 277 additions and 65 deletions
@@ -26,13 +26,14 @@ trait PersonalStatsScopeTrait
/**
* @param array<int, int> $creatorIds
* @return array{0: array<int, int>, 1: array<int, string>}
* @return array{0: array<int, int>, 1: array<int, string>, 2: array<int, string>}
* [adminId => deptId, deptId => name, deptId => "祖/父/当前"]
*/
protected static function loadAdminDeptMap(array $creatorIds): array
{
$creatorIds = array_values(array_unique(array_filter(array_map('intval', $creatorIds))));
if ($creatorIds === []) {
return [[], []];
return [[], [], []];
}
$rows = AdminDept::whereIn('admin_id', $creatorIds)
->field('admin_id, dept_id')
@@ -51,14 +52,57 @@ trait PersonalStatsScopeTrait
}
}
$deptNameMap = [];
$deptIds = array_values(array_unique($adminToDeptId));
if ($deptIds !== []) {
$deptNameMap = Dept::whereIn('id', $deptIds)
->column('name', 'id');
if ($adminToDeptId === []) {
return [[], [], []];
}
return [$adminToDeptId, $deptNameMap];
$allDeptRows = Dept::field('id, pid, name')->select()->toArray();
$deptIndex = [];
foreach ($allDeptRows as $row) {
$id = (int) ($row['id'] ?? 0);
if ($id <= 0) {
continue;
}
$deptIndex[$id] = [
'pid' => (int) ($row['pid'] ?? 0),
'name' => (string) ($row['name'] ?? ''),
];
}
$deptNameMap = [];
$deptPathMap = [];
foreach (array_unique(array_values($adminToDeptId)) as $deptId) {
$deptId = (int) $deptId;
if ($deptId <= 0 || !isset($deptIndex[$deptId])) {
continue;
}
$deptNameMap[$deptId] = $deptIndex[$deptId]['name'];
$deptPathMap[$deptId] = self::resolveDeptPath($deptId, $deptIndex);
}
return [$adminToDeptId, $deptNameMap, $deptPathMap];
}
/**
* 从根节点到当前部门的完整链路(用 / 分隔)。
*
* @param array<int, array{pid: int, name: string}> $deptIndex
*/
private static function resolveDeptPath(int $deptId, array $deptIndex): string
{
$names = [];
$guard = 0;
$cursor = $deptId;
while ($cursor > 0 && isset($deptIndex[$cursor]) && $guard++ < 32) {
$node = $deptIndex[$cursor];
$name = trim($node['name']);
if ($name !== '') {
array_unshift($names, $name);
}
$cursor = $node['pid'];
}
return implode(' / ', $names);
}
/**
@@ -71,12 +115,13 @@ trait PersonalStatsScopeTrait
return $rows;
}
$creatorIds = array_map(static fn (array $row): int => (int) ($row['creator_id'] ?? 0), $rows);
[$adminToDeptId, $deptNameMap] = self::loadAdminDeptMap($creatorIds);
[$adminToDeptId, $deptNameMap, $deptPathMap] = self::loadAdminDeptMap($creatorIds);
foreach ($rows as &$row) {
$creatorId = (int) ($row['creator_id'] ?? 0);
$deptId = $adminToDeptId[$creatorId] ?? 0;
$row['dept_id'] = $deptId;
$row['dept_name'] = $deptId > 0 ? (string) ($deptNameMap[$deptId] ?? '') : '';
$row['dept_path'] = $deptId > 0 ? (string) ($deptPathMap[$deptId] ?? '') : '';
}
unset($row);
@@ -154,27 +199,44 @@ trait PersonalStatsScopeTrait
return in_array($creatorId, $visibleIds, true);
}
protected static function isYejiDuplicate(int $creatorId, string $yejiDate, string $mediaSource, int $excludeId = 0): bool
/**
* 同一天 + 同一渠道全局唯一(不分录入人):避免重复汇总。
* 命中时返回首个已存在记录的录入人姓名,便于前端提示。
*
* @return array{conflict: bool, creator_name: string}
*/
protected static function findYejiConflict(string $yejiDate, string $mediaSource, int $excludeId = 0): array
{
$query = PersonalYeji::where('creator_id', $creatorId)
->where('yeji_date', $yejiDate)
$query = PersonalYeji::where('yeji_date', $yejiDate)
->where('media_source', $mediaSource);
if ($excludeId > 0) {
$query->where('id', '<>', $excludeId);
}
$row = $query->field('id, creator_name')->find();
if (!$row) {
return ['conflict' => false, 'creator_name' => ''];
}
return $query->count() > 0;
return ['conflict' => true, 'creator_name' => (string) ($row['creator_name'] ?? '')];
}
protected static function isCostDuplicate(int $creatorId, string $costDate, string $mediaSource, int $excludeId = 0): bool
/**
* 同一天 + 同一渠道全局唯一(不分录入人):避免重复汇总。
*
* @return array{conflict: bool, creator_name: string}
*/
protected static function findCostConflict(string $costDate, string $mediaSource, int $excludeId = 0): array
{
$query = PersonalAccountCost::where('creator_id', $creatorId)
->where('cost_date', $costDate)
$query = PersonalAccountCost::where('cost_date', $costDate)
->where('media_source', $mediaSource);
if ($excludeId > 0) {
$query->where('id', '<>', $excludeId);
}
$row = $query->field('id, creator_name')->find();
if (!$row) {
return ['conflict' => false, 'creator_name' => ''];
}
return $query->count() > 0;
return ['conflict' => true, 'creator_name' => (string) ($row['creator_name'] ?? '')];
}
}