fix(stats/self_input): 修复保存不入库/路由 404,补全部门与自媒体来源下拉
- 模型去掉 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>
This commit is contained in:
@@ -15,4 +15,14 @@ class SelfInputController extends BaseAdminController
|
||||
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自媒体来源下拉:从已录入业绩/账户消耗去重提取(随录入动态变化)
|
||||
*/
|
||||
public function mediaSourceOptions()
|
||||
{
|
||||
$list = SelfInputLogic::mediaSourceOptions($this->adminId, $this->adminInfo);
|
||||
|
||||
return $this->data($list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,19 @@ class AuthMiddleware
|
||||
return true;
|
||||
}
|
||||
|
||||
// 自录转化统计:自媒体来源下拉,复用总览或账户消耗列表权限
|
||||
if ($accessUri === 'stats.selfinput/mediasourceoptions') {
|
||||
$selfInputAliases = [
|
||||
'stats.selfinput/overview',
|
||||
'stats.self_input/overview',
|
||||
'stats.personalaccountcost/lists',
|
||||
'stats.personal_account_cost/lists',
|
||||
];
|
||||
if (count(array_intersect($selfInputAliases, $AdminUris)) > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 处方库列表:消费者开方页/处方库页导入共用,复用开方或处方库菜单权限
|
||||
if ($accessUri === 'tcm.prescriptionlibrary/lists'
|
||||
&& $this->matchPrescriptionLibraryListsPermission($adminUris)) {
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace app\adminapi\lists\stats;
|
||||
|
||||
use app\adminapi\lists\BaseAdminDataLists;
|
||||
use app\adminapi\logic\stats\PersonalStatsScopeTrait;
|
||||
use app\common\lists\ListsExtendInterface;
|
||||
use app\common\lists\ListsSearchInterface;
|
||||
use app\common\lists\Traits\HasDataScopeFilter;
|
||||
@@ -13,6 +14,7 @@ use app\common\model\stats\PersonalAccountCost;
|
||||
class PersonalAccountCostLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface
|
||||
{
|
||||
use HasDataScopeFilter;
|
||||
use PersonalStatsScopeTrait;
|
||||
|
||||
public function setSearch(): array
|
||||
{
|
||||
@@ -24,7 +26,15 @@ class PersonalAccountCostLists extends BaseAdminDataLists implements ListsSearch
|
||||
private function baseQuery()
|
||||
{
|
||||
$query = PersonalAccountCost::where($this->searchWhere);
|
||||
$this->applyDataScopeByOwner($query, 'creator_id');
|
||||
$visibleIds = $this->getDataScopeVisibleAdminIds();
|
||||
$deptId = (int) ($this->params['dept_id'] ?? 0);
|
||||
|
||||
$finalIds = self::intersectVisibleByDept($visibleIds, $deptId);
|
||||
if ($finalIds === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
} elseif ($finalIds !== null) {
|
||||
$query->whereIn('creator_id', $finalIds);
|
||||
}
|
||||
|
||||
if (!empty($this->params['start_date']) && !empty($this->params['end_date'])) {
|
||||
$query->whereBetween('cost_date', [$this->params['start_date'], $this->params['end_date']]);
|
||||
@@ -35,7 +45,7 @@ class PersonalAccountCostLists extends BaseAdminDataLists implements ListsSearch
|
||||
}
|
||||
|
||||
if (!empty($this->params['media_source'])) {
|
||||
$query->whereLike('media_source', '%' . trim((string) $this->params['media_source']) . '%');
|
||||
$query->where('media_source', trim((string) $this->params['media_source']));
|
||||
}
|
||||
|
||||
return $query;
|
||||
@@ -43,11 +53,13 @@ class PersonalAccountCostLists extends BaseAdminDataLists implements ListsSearch
|
||||
|
||||
public function lists(): array
|
||||
{
|
||||
return $this->baseQuery()
|
||||
$rows = $this->baseQuery()
|
||||
->order(['cost_date' => 'desc', 'id' => 'desc'])
|
||||
->limit($this->limitOffset, $this->limitLength)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
return self::attachDeptInfoToRows($rows);
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace app\adminapi\lists\stats;
|
||||
|
||||
use app\adminapi\lists\BaseAdminDataLists;
|
||||
use app\adminapi\logic\stats\PersonalStatsScopeTrait;
|
||||
use app\common\lists\ListsSearchInterface;
|
||||
use app\common\lists\Traits\HasDataScopeFilter;
|
||||
use app\common\model\stats\PersonalYeji;
|
||||
@@ -12,6 +13,7 @@ use app\common\model\stats\PersonalYeji;
|
||||
class PersonalYejiLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
{
|
||||
use HasDataScopeFilter;
|
||||
use PersonalStatsScopeTrait;
|
||||
|
||||
public function setSearch(): array
|
||||
{
|
||||
@@ -23,7 +25,15 @@ class PersonalYejiLists extends BaseAdminDataLists implements ListsSearchInterfa
|
||||
private function baseQuery()
|
||||
{
|
||||
$query = PersonalYeji::where($this->searchWhere);
|
||||
$this->applyDataScopeByOwner($query, 'creator_id');
|
||||
$visibleIds = $this->getDataScopeVisibleAdminIds();
|
||||
$deptId = (int) ($this->params['dept_id'] ?? 0);
|
||||
|
||||
$finalIds = self::intersectVisibleByDept($visibleIds, $deptId);
|
||||
if ($finalIds === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
} elseif ($finalIds !== null) {
|
||||
$query->whereIn('creator_id', $finalIds);
|
||||
}
|
||||
|
||||
if (!empty($this->params['start_date']) && !empty($this->params['end_date'])) {
|
||||
$query->whereBetween('yeji_date', [$this->params['start_date'], $this->params['end_date']]);
|
||||
@@ -34,7 +44,7 @@ class PersonalYejiLists extends BaseAdminDataLists implements ListsSearchInterfa
|
||||
}
|
||||
|
||||
if (!empty($this->params['media_source'])) {
|
||||
$query->whereLike('media_source', '%' . trim((string) $this->params['media_source']) . '%');
|
||||
$query->where('media_source', trim((string) $this->params['media_source']));
|
||||
}
|
||||
|
||||
if (!empty($this->params['creator_id'])) {
|
||||
@@ -46,11 +56,13 @@ class PersonalYejiLists extends BaseAdminDataLists implements ListsSearchInterfa
|
||||
|
||||
public function lists(): array
|
||||
{
|
||||
return $this->baseQuery()
|
||||
$rows = $this->baseQuery()
|
||||
->order(['yeji_date' => 'desc', 'id' => 'desc'])
|
||||
->limit($this->limitOffset, $this->limitLength)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
return self::attachDeptInfoToRows($rows);
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
|
||||
@@ -64,12 +64,6 @@ class PersonalAccountCostLogic extends BaseLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!self::canMutateRecord($adminId, $adminInfo, (int) $model->creator_id)) {
|
||||
self::setError('仅可编辑本人录入的账户消耗');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$model->amount = round((float) ($params['amount'] ?? 0), 2);
|
||||
$model->remark = (string) ($params['remark'] ?? '');
|
||||
$model->updater_id = $adminId;
|
||||
@@ -100,12 +94,6 @@ class PersonalAccountCostLogic extends BaseLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!self::canMutateRecord($adminId, $adminInfo, (int) $model->creator_id)) {
|
||||
self::setError('仅可删除本人录入的账户消耗');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$model->delete();
|
||||
|
||||
return true;
|
||||
|
||||
@@ -4,10 +4,13 @@ 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
|
||||
{
|
||||
@@ -21,26 +24,124 @@ trait PersonalStatsScopeTrait
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>|null
|
||||
* 与 project.self_input_stats_view_all_roles 一致:超管或白名单角色可见全部录入人数据。
|
||||
*/
|
||||
protected static function getVisibleCreatorIds(int $adminId, array $adminInfo): ?array
|
||||
{
|
||||
return DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||||
}
|
||||
|
||||
protected static function canMutateRecord(int $adminId, array $adminInfo, int $creatorId): bool
|
||||
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 $adminId > 0 && $adminId === $creatorId;
|
||||
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
|
||||
|
||||
@@ -71,12 +71,6 @@ class PersonalYejiLogic extends BaseLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!self::canMutateRecord($adminId, $adminInfo, (int) $model->creator_id)) {
|
||||
self::setError('仅可编辑本人录入的业绩');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$model->add_fans_count = (int) ($params['add_fans_count'] ?? 0);
|
||||
$model->total_open_count = (int) ($params['total_open_count'] ?? 0);
|
||||
$model->unreplied_count = (int) ($params['unreplied_count'] ?? 0);
|
||||
@@ -114,12 +108,6 @@ class PersonalYejiLogic extends BaseLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!self::canMutateRecord($adminId, $adminInfo, (int) $model->creator_id)) {
|
||||
self::setError('仅可删除本人录入的业绩');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$model->delete();
|
||||
|
||||
return true;
|
||||
|
||||
@@ -18,10 +18,13 @@ class SelfInputLogic extends BaseLogic
|
||||
$pageNo = max(1, (int) ($params['page_no'] ?? 1));
|
||||
$pageSize = min(100, max(1, (int) ($params['page_size'] ?? 15)));
|
||||
$mediaSource = trim((string) ($params['media_source'] ?? ''));
|
||||
$deptId = (int) ($params['dept_id'] ?? 0);
|
||||
|
||||
$yejiQuery = self::buildYejiQuery($startDate, $endDate, $adminId, $adminInfo);
|
||||
$effectiveAdminIds = self::resolveEffectiveAdminIds($adminId, $adminInfo, $deptId);
|
||||
|
||||
$yejiQuery = self::buildYejiQuery($startDate, $endDate, $effectiveAdminIds);
|
||||
if ($mediaSource !== '') {
|
||||
$yejiQuery->whereLike('media_source', '%' . $mediaSource . '%');
|
||||
$yejiQuery->where('media_source', $mediaSource);
|
||||
}
|
||||
|
||||
$count = (int) (clone $yejiQuery)->count();
|
||||
@@ -31,7 +34,7 @@ class SelfInputLogic extends BaseLogic
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$costMap = self::loadAccountCostMap($startDate, $endDate, $adminId, $adminInfo, $mediaSource);
|
||||
$costMap = self::loadAccountCostMap($startDate, $endDate, $effectiveAdminIds, $mediaSource);
|
||||
|
||||
$lists = [];
|
||||
foreach ($rows as $row) {
|
||||
@@ -44,10 +47,11 @@ class SelfInputLogic extends BaseLogic
|
||||
$entity['account_cost'] = round((float) ($costMap[$costKey] ?? 0), 2);
|
||||
$lists[] = self::finalizeMetrics($entity);
|
||||
}
|
||||
$lists = self::attachDeptInfoToRows($lists);
|
||||
|
||||
$allYejiRows = self::buildYejiQuery($startDate, $endDate, $adminId, $adminInfo);
|
||||
$allYejiRows = self::buildYejiQuery($startDate, $endDate, $effectiveAdminIds);
|
||||
if ($mediaSource !== '') {
|
||||
$allYejiRows->whereLike('media_source', '%' . $mediaSource . '%');
|
||||
$allYejiRows->where('media_source', $mediaSource);
|
||||
}
|
||||
$allYeji = $allYejiRows->select()->toArray();
|
||||
|
||||
@@ -85,39 +89,83 @@ class SelfInputLogic extends BaseLogic
|
||||
];
|
||||
}
|
||||
|
||||
private static function buildYejiQuery(string $startDate, string $endDate, int $adminId, array $adminInfo)
|
||||
/**
|
||||
* 从已录入的业绩/账户消耗中提取去重后的自媒体来源(与列表数据权限一致)。
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function mediaSourceOptions(int $adminId, array $adminInfo): array
|
||||
{
|
||||
$effectiveAdminIds = self::resolveEffectiveAdminIds($adminId, $adminInfo, 0);
|
||||
if ($effectiveAdminIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$list = array_keys($map);
|
||||
sort($list, SORT_STRING);
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>|null null = 不限;[] = 无可见
|
||||
*/
|
||||
private static function resolveEffectiveAdminIds(int $adminId, array $adminInfo, int $deptId): ?array
|
||||
{
|
||||
$visibleIds = self::getVisibleCreatorIds($adminId, $adminInfo);
|
||||
|
||||
return self::intersectVisibleByDept($visibleIds, $deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int>|null $effectiveAdminIds
|
||||
*/
|
||||
private static function buildYejiQuery(string $startDate, string $endDate, ?array $effectiveAdminIds)
|
||||
{
|
||||
$query = PersonalYeji::whereBetween('yeji_date', [$startDate, $endDate]);
|
||||
$visibleIds = self::getVisibleCreatorIds($adminId, $adminInfo);
|
||||
if ($visibleIds === []) {
|
||||
if ($effectiveAdminIds === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
} elseif ($visibleIds !== null) {
|
||||
$query->whereIn('creator_id', $visibleIds);
|
||||
} elseif ($effectiveAdminIds !== null) {
|
||||
$query->whereIn('creator_id', $effectiveAdminIds);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int>|null $effectiveAdminIds
|
||||
* @return array<string, float>
|
||||
*/
|
||||
private static function loadAccountCostMap(
|
||||
string $startDate,
|
||||
string $endDate,
|
||||
int $adminId,
|
||||
array $adminInfo,
|
||||
?array $effectiveAdminIds,
|
||||
string $mediaSource
|
||||
): array {
|
||||
$query = PersonalAccountCost::whereBetween('cost_date', [$startDate, $endDate]);
|
||||
$visibleIds = self::getVisibleCreatorIds($adminId, $adminInfo);
|
||||
if ($visibleIds === []) {
|
||||
if ($effectiveAdminIds === []) {
|
||||
return [];
|
||||
}
|
||||
if ($visibleIds !== null) {
|
||||
$query->whereIn('creator_id', $visibleIds);
|
||||
if ($effectiveAdminIds !== null) {
|
||||
$query->whereIn('creator_id', $effectiveAdminIds);
|
||||
}
|
||||
if ($mediaSource !== '') {
|
||||
$query->whereLike('media_source', '%' . $mediaSource . '%');
|
||||
$query->where('media_source', $mediaSource);
|
||||
}
|
||||
|
||||
$rows = $query
|
||||
|
||||
Reference in New Issue
Block a user