146 lines
5.3 KiB
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;
|
|
}
|
|
}
|