- 模型去掉 defaultSoftDelete=0,避免 ThinkPHP 软删过滤导致新增记录"隐形" - 菜单 paths 改相对路径并新增「自录数据统计」目录,迁移已有菜单 - Lists/Overview 补部门关联、支持按部门筛选 - 数据权限改由 self_input_stats_view_all_roles 白名单 + DataScope 控制, 编辑/删除完全交给按钮权限校验(移除"仅本人可改"硬限制) - 新增 /stats.self_input/mediaSourceOptions 接口,统计页/账户消耗页/录入弹窗 共用 useMediaSourceOptions + MediaSourceSelect,去重动态加载 Co-authored-by: Cursor <cursoragent@cursor.com>
181 lines
5.8 KiB
PHP
181 lines
5.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace app\adminapi\logic\stats;
|
|
|
|
use app\adminapi\logic\dept\DeptLogic;
|
|
use app\common\model\auth\AdminDept;
|
|
use app\common\model\dept\Dept;
|
|
use app\common\model\stats\PersonalAccountCost;
|
|
use app\common\model\stats\PersonalYeji;
|
|
use app\common\service\DataScope\DataScopeService;
|
|
use think\facade\Config;
|
|
|
|
trait PersonalStatsScopeTrait
|
|
{
|
|
protected static function resolvePrimaryDeptId(int $adminId): int
|
|
{
|
|
if ($adminId <= 0) {
|
|
return 0;
|
|
}
|
|
$deptId = AdminDept::where('admin_id', $adminId)->value('dept_id');
|
|
|
|
return (int) ($deptId ?: 0);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, int> $creatorIds
|
|
* @return array{0: array<int, int>, 1: array<int, string>}
|
|
*/
|
|
protected static function loadAdminDeptMap(array $creatorIds): array
|
|
{
|
|
$creatorIds = array_values(array_unique(array_filter(array_map('intval', $creatorIds))));
|
|
if ($creatorIds === []) {
|
|
return [[], []];
|
|
}
|
|
$rows = AdminDept::whereIn('admin_id', $creatorIds)
|
|
->field('admin_id, dept_id')
|
|
->select()
|
|
->toArray();
|
|
|
|
$adminToDeptId = [];
|
|
foreach ($rows as $row) {
|
|
$adminId = (int) ($row['admin_id'] ?? 0);
|
|
$deptId = (int) ($row['dept_id'] ?? 0);
|
|
if ($adminId <= 0 || $deptId <= 0) {
|
|
continue;
|
|
}
|
|
if (!isset($adminToDeptId[$adminId])) {
|
|
$adminToDeptId[$adminId] = $deptId;
|
|
}
|
|
}
|
|
|
|
$deptNameMap = [];
|
|
$deptIds = array_values(array_unique($adminToDeptId));
|
|
if ($deptIds !== []) {
|
|
$deptNameMap = Dept::whereIn('id', $deptIds)
|
|
->column('name', 'id');
|
|
}
|
|
|
|
return [$adminToDeptId, $deptNameMap];
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $rows
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
protected static function attachDeptInfoToRows(array $rows): array
|
|
{
|
|
if ($rows === []) {
|
|
return $rows;
|
|
}
|
|
$creatorIds = array_map(static fn (array $row): int => (int) ($row['creator_id'] ?? 0), $rows);
|
|
[$adminToDeptId, $deptNameMap] = 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] ?? '') : '';
|
|
}
|
|
unset($row);
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* 根据 dept_id(包含其子部门)收窄可见 admin id 集合。
|
|
* 与 visibleAdminIds 取交集。
|
|
*
|
|
* @param array<int>|null $visibleAdminIds null 表示不限
|
|
* @return array<int>|null 返回 null 表示外部条件无需附加;返回 [] 表示无可见 admin
|
|
*/
|
|
protected static function intersectVisibleByDept(?array $visibleAdminIds, int $deptId): ?array
|
|
{
|
|
if ($deptId <= 0) {
|
|
return $visibleAdminIds;
|
|
}
|
|
$deptIds = DeptLogic::getSelfAndDescendantIds($deptId);
|
|
if ($deptIds === []) {
|
|
$deptIds = [$deptId];
|
|
}
|
|
$adminIds = AdminDept::whereIn('dept_id', $deptIds)->column('admin_id');
|
|
$adminIds = array_values(array_unique(array_filter(array_map('intval', $adminIds), static fn (int $v): bool => $v > 0)));
|
|
|
|
if ($visibleAdminIds === null) {
|
|
return $adminIds;
|
|
}
|
|
|
|
return array_values(array_intersect($visibleAdminIds, $adminIds));
|
|
}
|
|
|
|
protected static function normalizeMediaSource(string $mediaSource): string
|
|
{
|
|
return trim($mediaSource);
|
|
}
|
|
|
|
/**
|
|
* 与 project.self_input_stats_view_all_roles 一致:超管或白名单角色可见全部录入人数据。
|
|
*/
|
|
protected static function canViewAllSelfInputStats(array $adminInfo): bool
|
|
{
|
|
if ((int) ($adminInfo['root'] ?? 0) === 1) {
|
|
return true;
|
|
}
|
|
$allow = Config::get('project.self_input_stats_view_all_roles', []);
|
|
$allow = array_map('intval', is_array($allow) ? $allow : []);
|
|
if ($allow === []) {
|
|
return false;
|
|
}
|
|
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
|
|
|
|
return count(array_intersect($myRoles, $allow)) > 0;
|
|
}
|
|
|
|
/**
|
|
* @return array<int>|null null=全部录入人;[]=无可见;int[]=可见 creator_id 集合
|
|
*/
|
|
protected static function getVisibleCreatorIds(int $adminId, array $adminInfo): ?array
|
|
{
|
|
if (self::canViewAllSelfInputStats($adminInfo)) {
|
|
return null;
|
|
}
|
|
|
|
return DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
|
}
|
|
|
|
protected static function assertRecordVisible(int $adminId, array $adminInfo, int $creatorId): bool
|
|
{
|
|
$visibleIds = self::getVisibleCreatorIds($adminId, $adminInfo);
|
|
if ($visibleIds === null) {
|
|
return true;
|
|
}
|
|
|
|
return in_array($creatorId, $visibleIds, true);
|
|
}
|
|
|
|
protected static function isYejiDuplicate(int $creatorId, string $yejiDate, string $mediaSource, int $excludeId = 0): bool
|
|
{
|
|
$query = PersonalYeji::where('creator_id', $creatorId)
|
|
->where('yeji_date', $yejiDate)
|
|
->where('media_source', $mediaSource);
|
|
if ($excludeId > 0) {
|
|
$query->where('id', '<>', $excludeId);
|
|
}
|
|
|
|
return $query->count() > 0;
|
|
}
|
|
|
|
protected static function isCostDuplicate(int $creatorId, string $costDate, string $mediaSource, int $excludeId = 0): bool
|
|
{
|
|
$query = PersonalAccountCost::where('creator_id', $creatorId)
|
|
->where('cost_date', $costDate)
|
|
->where('media_source', $mediaSource);
|
|
if ($excludeId > 0) {
|
|
$query->where('id', '<>', $excludeId);
|
|
}
|
|
|
|
return $query->count() > 0;
|
|
}
|
|
}
|