Files
zyt/server/app/adminapi/logic/stats/SelfInputLogic.php
T
longandCursor bba989c0af 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>
2026-05-22 11:47:15 +08:00

358 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace app\adminapi\logic\stats;
use app\common\logic\BaseLogic;
use app\common\model\stats\PersonalAccountCost;
use app\common\model\stats\PersonalYeji;
class SelfInputLogic extends BaseLogic
{
use PersonalStatsScopeTrait;
public static function overview(array $params, int $adminId, array $adminInfo): array
{
[$startDate, $endDate] = self::resolveTimeRange($params);
$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);
$effectiveAdminIds = self::resolveEffectiveAdminIds($adminId, $adminInfo, $deptId);
$yejiQuery = self::buildYejiQuery($startDate, $endDate, $effectiveAdminIds);
if ($mediaSource !== '') {
$yejiQuery->where('media_source', $mediaSource);
}
$count = (int) (clone $yejiQuery)->count();
$rows = (clone $yejiQuery)
->order(['yeji_date' => 'desc', 'id' => 'desc'])
->page($pageNo, $pageSize)
->select()
->toArray();
$costMap = self::loadAccountCostMap($startDate, $endDate, $effectiveAdminIds, $mediaSource);
$lists = [];
foreach ($rows as $row) {
$entity = self::normalizeYejiRow($row);
$costKey = self::buildCostKey(
(int) $entity['creator_id'],
(string) $entity['yeji_date'],
(string) $entity['media_source']
);
$entity['account_cost'] = round((float) ($costMap[$costKey] ?? 0), 2);
$lists[] = self::finalizeMetrics($entity);
}
$lists = self::attachDeptInfoToRows($lists);
$allYejiRows = self::buildYejiQuery($startDate, $endDate, $effectiveAdminIds);
if ($mediaSource !== '') {
$allYejiRows->where('media_source', $mediaSource);
}
$allYeji = $allYejiRows->select()->toArray();
$summaryCostMap = $costMap;
$summaryBase = self::emptyMetrics();
foreach ($allYeji as $row) {
$entity = self::normalizeYejiRow($row);
$costKey = self::buildCostKey(
(int) $entity['creator_id'],
(string) $entity['yeji_date'],
(string) $entity['media_source']
);
$entity['account_cost'] = round((float) ($summaryCostMap[$costKey] ?? 0), 2);
unset($summaryCostMap[$costKey]);
$entity = self::finalizeMetrics($entity);
$summaryBase = self::accumulateMetrics($summaryBase, $entity);
}
foreach ($summaryCostMap as $amount) {
$summaryBase['account_cost'] += round((float) $amount, 2);
}
$summary = self::finalizeMetrics($summaryBase);
return [
'summary' => $summary,
'lists' => $lists,
'count' => $count,
'page_no' => $pageNo,
'page_size' => $pageSize,
'extend' => [
'summary' => $summary,
'date_range' => [$startDate, $endDate],
],
];
}
/**
* 从已录入的业绩/账户消耗中提取去重后的自媒体来源(与列表数据权限一致)。
*
* @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]);
if ($effectiveAdminIds === []) {
$query->whereRaw('0 = 1');
} 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,
?array $effectiveAdminIds,
string $mediaSource
): array {
$query = PersonalAccountCost::whereBetween('cost_date', [$startDate, $endDate]);
if ($effectiveAdminIds === []) {
return [];
}
if ($effectiveAdminIds !== null) {
$query->whereIn('creator_id', $effectiveAdminIds);
}
if ($mediaSource !== '') {
$query->where('media_source', $mediaSource);
}
$rows = $query
->fieldRaw('creator_id, cost_date, media_source, SUM(amount) AS total_amount')
->group('creator_id, cost_date, media_source')
->select()
->toArray();
$map = [];
foreach ($rows as $row) {
$key = self::buildCostKey(
(int) $row['creator_id'],
(string) $row['cost_date'],
(string) $row['media_source']
);
$map[$key] = round((float) ($row['total_amount'] ?? 0), 2);
}
return $map;
}
private static function buildCostKey(int $creatorId, string $date, string $mediaSource): string
{
return $creatorId . '|' . $date . '|' . self::normalizeMediaSource($mediaSource);
}
/**
* @param array<string, mixed> $row
* @return array<string, mixed>
*/
private static function normalizeYejiRow(array $row): array
{
return [
'id' => (int) ($row['id'] ?? 0),
'yeji_date' => (string) ($row['yeji_date'] ?? ''),
'media_source' => (string) ($row['media_source'] ?? ''),
'creator_id' => (int) ($row['creator_id'] ?? 0),
'creator_name' => (string) ($row['creator_name'] ?? ''),
'remark' => (string) ($row['remark'] ?? ''),
'add_fans_count' => (int) ($row['add_fans_count'] ?? 0),
'total_open_count' => (int) ($row['total_open_count'] ?? 0),
'unreplied_count' => (int) ($row['unreplied_count'] ?? 0),
'paid_appointment_count' => (int) ($row['paid_appointment_count'] ?? 0),
'free_appointment_count' => (int) ($row['free_appointment_count'] ?? 0),
'interview_count' => (int) ($row['interview_count'] ?? 0),
'order_amount' => round((float) ($row['order_amount'] ?? 0), 2),
'completed_order_count' => (int) ($row['completed_order_count'] ?? 0),
'account_cost' => 0.0,
];
}
/**
* @return array<string, mixed>
*/
private static function emptyMetrics(): array
{
return [
'add_fans_count' => 0,
'total_open_count' => 0,
'unreplied_count' => 0,
'paid_appointment_count' => 0,
'free_appointment_count' => 0,
'appointment_total_count' => 0,
'interview_count' => 0,
'order_amount' => 0.0,
'completed_order_count' => 0,
'account_cost' => 0.0,
];
}
/**
* @param array<string, mixed> $base
* @param array<string, mixed> $row
* @return array<string, mixed>
*/
private static function accumulateMetrics(array $base, array $row): array
{
$base['add_fans_count'] += (int) ($row['add_fans_count'] ?? 0);
$base['total_open_count'] += (int) ($row['total_open_count'] ?? 0);
$base['unreplied_count'] += (int) ($row['unreplied_count'] ?? 0);
$base['paid_appointment_count'] += (int) ($row['paid_appointment_count'] ?? 0);
$base['free_appointment_count'] += (int) ($row['free_appointment_count'] ?? 0);
$base['interview_count'] += (int) ($row['interview_count'] ?? 0);
$base['order_amount'] = round((float) $base['order_amount'] + (float) ($row['order_amount'] ?? 0), 2);
$base['completed_order_count'] += (int) ($row['completed_order_count'] ?? 0);
$base['account_cost'] = round((float) $base['account_cost'] + (float) ($row['account_cost'] ?? 0), 2);
return $base;
}
/**
* @param array<string, mixed> $entity
* @return array<string, mixed>
*/
private static function finalizeMetrics(array $entity): array
{
$paidAppointmentCount = (int) ($entity['paid_appointment_count'] ?? 0);
$freeAppointmentCount = (int) ($entity['free_appointment_count'] ?? 0);
$appointmentTotalCount = $paidAppointmentCount + $freeAppointmentCount;
$interviewCount = (int) ($entity['interview_count'] ?? 0);
$addFansCount = (int) ($entity['add_fans_count'] ?? 0);
$totalOpenCount = (int) ($entity['total_open_count'] ?? 0);
$completedOrderCount = (int) ($entity['completed_order_count'] ?? 0);
$orderAmount = round((float) ($entity['order_amount'] ?? 0), 2);
$accountCost = round((float) ($entity['account_cost'] ?? 0), 2);
$entity['appointment_total_count'] = $appointmentTotalCount;
$entity['order_amount'] = $orderAmount;
$entity['account_cost'] = $accountCost;
$entity['paid_appointment_rate'] = self::percent($paidAppointmentCount, $addFansCount);
$entity['open_appointment_rate'] = self::percent($paidAppointmentCount, $totalOpenCount);
$entity['interview_rate'] = self::percent($interviewCount, $appointmentTotalCount);
$entity['receive_rate'] = self::percent($completedOrderCount, $addFansCount);
$entity['interview_receive_rate'] = self::percent($completedOrderCount, $interviewCount);
$entity['open_receive_rate'] = self::percent($completedOrderCount, $totalOpenCount);
$entity['avg_unit_price'] = self::safeDivideMoney($orderAmount, $completedOrderCount);
$entity['cash_cost'] = self::safeDivideMoney($accountCost, $addFansCount);
$entity['roi'] = self::safeDivideRatio($orderAmount, $accountCost);
return $entity;
}
/**
* @return array{0: string, 1: string}
*/
private static function resolveTimeRange(array $params): array
{
$today = date('Y-m-d');
$timeType = (string) ($params['time_type'] ?? 'today');
switch ($timeType) {
case 'yesterday':
$startDate = date('Y-m-d', strtotime('-1 day'));
$endDate = $startDate;
break;
case 'week':
$startDate = date('Y-m-d', strtotime('-6 days'));
$endDate = $today;
break;
case 'month':
$startDate = date('Y-m-d', strtotime('-29 days'));
$endDate = $today;
break;
case 'custom':
$startDate = trim((string) ($params['start_date'] ?? ''));
$endDate = trim((string) ($params['end_date'] ?? ''));
if ($startDate === '' || $endDate === '') {
$startDate = $today;
$endDate = $today;
} elseif ($startDate > $endDate) {
[$startDate, $endDate] = [$endDate, $startDate];
}
break;
case 'today':
default:
$startDate = $today;
$endDate = $today;
}
return [$startDate, $endDate];
}
private static function percent(int $numerator, int $denominator): float
{
if ($denominator <= 0) {
return 0.0;
}
return round(($numerator / $denominator) * 100, 2);
}
private static function safeDivideMoney(float $numerator, int $denominator): float
{
if ($denominator <= 0) {
return 0.0;
}
return round($numerator / $denominator, 2);
}
private static function safeDivideRatio(float $numerator, float $denominator): float
{
if ($denominator <= 0) {
return 0.0;
}
return round($numerator / $denominator, 2);
}
}