fix(stats/self_input): 渠道字典、财务脱敏与同日同渠道全局唯一
自媒体来源改推广渠道字典;非白名单隐藏账户消耗/ROI;部门展示完整路径;业绩与消耗按日期+渠道全局去重。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -22,8 +22,10 @@ class PersonalAccountCostLogic extends BaseLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self::isCostDuplicate($adminId, $costDate, $mediaSource)) {
|
||||
self::setError('该日期下该自媒体来源的账户消耗已存在,请直接编辑');
|
||||
$conflict = self::findCostConflict($costDate, $mediaSource);
|
||||
if ($conflict['conflict']) {
|
||||
$by = $conflict['creator_name'] !== '' ? '(录入人:' . $conflict['creator_name'] . ')' : '';
|
||||
self::setError('该日期下该渠道的账户消耗已存在' . $by . ',请直接编辑该记录,避免重复汇总');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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'] ?? '')];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,10 @@ class PersonalYejiLogic extends BaseLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self::isYejiDuplicate($adminId, $yejiDate, $mediaSource)) {
|
||||
self::setError('该日期下该自媒体来源的业绩已存在,请直接编辑');
|
||||
$conflict = self::findYejiConflict($yejiDate, $mediaSource);
|
||||
if ($conflict['conflict']) {
|
||||
$by = $conflict['creator_name'] !== '' ? '(录入人:' . $conflict['creator_name'] . ')' : '';
|
||||
self::setError('该日期下该渠道的业绩已存在' . $by . ',请直接编辑该记录,避免重复汇总');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace app\adminapi\logic\stats;
|
||||
|
||||
use app\common\logic\BaseLogic;
|
||||
use app\common\model\dict\DictData;
|
||||
use app\common\model\stats\PersonalAccountCost;
|
||||
use app\common\model\stats\PersonalYeji;
|
||||
|
||||
@@ -75,6 +76,15 @@ class SelfInputLogic extends BaseLogic
|
||||
}
|
||||
|
||||
$summary = self::finalizeMetrics($summaryBase);
|
||||
$canViewFinance = self::canViewAllSelfInputStats($adminInfo);
|
||||
|
||||
if (!$canViewFinance) {
|
||||
$summary = self::maskFinanceFields($summary);
|
||||
foreach ($lists as &$item) {
|
||||
$item = self::maskFinanceFields($item);
|
||||
}
|
||||
unset($item);
|
||||
}
|
||||
|
||||
return [
|
||||
'summary' => $summary,
|
||||
@@ -85,39 +95,56 @@ class SelfInputLogic extends BaseLogic
|
||||
'extend' => [
|
||||
'summary' => $summary,
|
||||
'date_range' => [$startDate, $endDate],
|
||||
'can_view_finance' => $canViewFinance,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从已录入的业绩/账户消耗中提取去重后的自媒体来源(与列表数据权限一致)。
|
||||
* 财务相关字段(账户消耗 / 现金成本 / ROI):非豁免角色不可见,剔除字段避免被嗅探。
|
||||
*
|
||||
* @return string[]
|
||||
* @param array<string, mixed> $entity
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function maskFinanceFields(array $entity): array
|
||||
{
|
||||
foreach (['account_cost', 'cash_cost', 'roi'] as $key) {
|
||||
unset($entity[$key]);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自媒体来源选项:来自字典「推广渠道」(type_value=channels),按 sort/id 排序。
|
||||
* 用 dict_data.name 作为存储值(与历史录入兼容;保留 value 仅作展示标识)。
|
||||
*
|
||||
* @return array<int, array{name: string, value: string}>
|
||||
*/
|
||||
public static function mediaSourceOptions(int $adminId, array $adminInfo): array
|
||||
{
|
||||
$effectiveAdminIds = self::resolveEffectiveAdminIds($adminId, $adminInfo, 0);
|
||||
if ($effectiveAdminIds === []) {
|
||||
return [];
|
||||
}
|
||||
unset($adminId, $adminInfo);
|
||||
|
||||
$map = [];
|
||||
foreach ([PersonalYeji::class, PersonalAccountCost::class] as $modelClass) {
|
||||
$query = $modelClass::where('media_source', '<>', '');
|
||||
if ($effectiveAdminIds !== null) {
|
||||
$query->whereIn('creator_id', $effectiveAdminIds);
|
||||
}
|
||||
$rows = $query->group('media_source')->column('media_source');
|
||||
foreach ($rows as $raw) {
|
||||
$norm = self::normalizeMediaSource((string) $raw);
|
||||
if ($norm !== '') {
|
||||
$map[$norm] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
$rows = DictData::where('type_value', 'channels')
|
||||
->where('status', 1)
|
||||
->order(['sort' => 'desc', 'id' => 'asc'])
|
||||
->field('name, value')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$list = array_keys($map);
|
||||
sort($list, SORT_STRING);
|
||||
$list = [];
|
||||
$seen = [];
|
||||
foreach ($rows as $row) {
|
||||
$name = self::normalizeMediaSource((string) ($row['name'] ?? ''));
|
||||
if ($name === '' || isset($seen[$name])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$name] = true;
|
||||
$list[] = [
|
||||
'name' => $name,
|
||||
'value' => (string) ($row['value'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user