CaiWuXiTong/modules/EcommerceFinance/Services/DingWorklogPenaltyCalculator.php

146 lines
5.3 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use App\Services\Auth\FinanceRoster;
use Carbon\CarbonInterface;
use Illuminate\Support\Carbon;
use Modules\EcommerceFinance\Repositories\DingWorklogMirrorRepository;
class DingWorklogPenaltyCalculator
{
private const BASE_POINTS = '10';
public function __construct(
private readonly DingWorklogMirrorRepository $repository,
private readonly FinanceRoster $roster,
) {}
/**
* @return array{
* start_date:?string,
* end_date:?string,
* total_points:string,
* total_points_formatted:string,
* members:list<array{
* userid:string,
* name:string,
* missed_dates:list<string>,
* missed_count:int,
* penalty_points:string,
* penalty_points_formatted:string,
* penalty_formula:string
* }>
* }
*/
public function summary(string $from, string $to, CarbonInterface $at): array
{
$timezone = (string) config('app.timezone', 'Asia/Shanghai');
$now = Carbon::instance($at)->timezone($timezone);
$requestedStart = Carbon::parse($from, $timezone)->startOfDay();
$requestedEnd = Carbon::parse($to, $timezone)->startOfDay();
$configuredStart = Carbon::parse(
(string) config('dingtalk.worklog.penalty_start_date', '2026-11-04'),
$timezone,
)->startOfDay();
$latestClosed = $now->hour < 12
? $now->subDays(2)->startOfDay()
: $now->subDay()->startOfDay();
$end = $requestedEnd->lessThan($latestClosed) ? $requestedEnd->copy() : $latestClosed;
$earliest = $this->repository->earliestStatisticsDate();
if ($earliest === null) {
return [
'start_date' => null,
'end_date' => null,
'total_points' => '0',
'total_points_formatted' => '0',
'members' => [],
];
}
$earliestDate = Carbon::parse($earliest, $timezone)->startOfDay();
$start = $requestedStart->greaterThan($earliestDate) ? $requestedStart->copy() : $earliestDate;
if ($configuredStart->greaterThan($start)) {
$start = $configuredStart;
}
if ($start->greaterThan($end)) {
return [
'start_date' => null,
'end_date' => null,
'total_points' => '0',
'total_points_formatted' => '0',
'members' => [],
];
}
$submitted = [];
foreach ($this->repository->submittedDatesByUser($start->toDateString(), $end->toDateString()) as $row) {
if ($row['creator_userid'] !== '') {
$submitted[$row['creator_userid']][$row['statistics_date']] = true;
}
}
$members = [];
foreach ($this->roster->cachedFinanceMembers() as $member) {
$userId = trim((string) ($member['userid'] ?? ''));
if ($userId === '') {
continue;
}
$members[$userId] = [
'userid' => $userId,
'name' => trim((string) ($member['name'] ?? '')) ?: $userId,
'missed_dates' => [],
'missed_count' => 0,
'penalty_points' => '0',
'penalty_formula' => [],
];
}
foreach ($members as $userId => &$member) {
$sequence = 0;
for ($date = $start->copy(); $date->lessThanOrEqualTo($end); $date->addDay()) {
$dateKey = $date->toDateString();
if (isset($submitted[$userId][$dateKey])) {
continue;
}
$sequence++;
$increment = bcmul(self::BASE_POINTS, bcpow('2', (string) ($sequence - 1), 0), 0);
$member['penalty_points'] = bcadd($member['penalty_points'], $increment, 0);
$member['penalty_formula'][] = $increment;
$member['missed_dates'][] = $dateKey;
$member['missed_count']++;
}
}
unset($member);
$rows = array_values(array_filter($members, static fn (array $member): bool => $member['missed_count'] > 0));
usort($rows, static fn (array $left, array $right): int =>
(bccomp($right['penalty_points'], $left['penalty_points']) ?: strcmp($left['name'], $right['name']))
);
$totalPoints = array_reduce(
$rows,
static fn (string $total, array $member): string => bcadd($total, $member['penalty_points'], 0),
'0',
);
return [
'start_date' => $start->toDateString(),
'end_date' => $end->toDateString(),
'total_points' => $totalPoints,
'total_points_formatted' => $this->formatPoints($totalPoints),
'members' => array_map(function (array $member): array {
$member['penalty_points_formatted'] = $this->formatPoints($member['penalty_points']);
$member['penalty_formula'] = implode(' + ', $member['penalty_formula']);
return $member;
}, $rows),
];
}
private function formatPoints(string $points): string
{
return preg_replace('/\B(?=(\d{3})+(?!\d))/', ',', $points) ?: $points;
}
}