Files
2026-04-26 09:24:08 +08:00

296 lines
8.7 KiB
PHP

<?php
declare(strict_types=1);
namespace app\smt\service;
use DateInterval;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
final class Support
{
public const DATE_LAYOUT = 'Y-m-d';
public const DATETIME_LAYOUT = 'Y-m-d H:i:s';
public static function tz(): DateTimeZone
{
return new DateTimeZone((string) config('app.default_timezone', 'Asia/Shanghai'));
}
public static function now(): DateTimeImmutable
{
return new DateTimeImmutable('now', self::tz());
}
public static function dateOnly($value = null): DateTimeImmutable
{
$dt = self::toDateTime($value);
return $dt->setTime(0, 0, 0);
}
public static function parseDate(?string $value, string $field = 'date'): ?DateTimeImmutable
{
$text = trim((string) $value);
if ($text === '') {
return null;
}
$dt = DateTimeImmutable::createFromFormat(self::DATE_LAYOUT, $text, self::tz());
if (!$dt || $dt->format(self::DATE_LAYOUT) !== $text) {
throw new \RuntimeException(sprintf('%s 格式错误,应为 YYYY-MM-DD', $field), 400);
}
return $dt->setTime(0, 0, 0);
}
public static function parseDateTime(?string $value, string $field = 'datetime'): ?DateTimeImmutable
{
$text = trim((string) $value);
if ($text === '') {
return null;
}
$dt = DateTimeImmutable::createFromFormat(self::DATETIME_LAYOUT, $text, self::tz());
if ($dt && $dt->format(self::DATETIME_LAYOUT) === $text) {
return $dt;
}
try {
return (new DateTimeImmutable($text))->setTimezone(self::tz());
} catch (\Throwable $e) {
throw new \RuntimeException(sprintf('%s 格式错误,应为 YYYY-MM-DD HH:MM:SS 或 RFC3339', $field), 400);
}
}
public static function toDateTime($value): DateTimeImmutable
{
if ($value instanceof DateTimeImmutable) {
return $value->setTimezone(self::tz());
}
if ($value instanceof DateTimeInterface) {
return (new DateTimeImmutable($value->format(DateTimeInterface::ATOM)))->setTimezone(self::tz());
}
if (is_int($value)) {
return (new DateTimeImmutable('@' . $value))->setTimezone(self::tz());
}
if (is_numeric($value) && (string) (int) $value === (string) $value) {
return (new DateTimeImmutable('@' . (int) $value))->setTimezone(self::tz());
}
$text = trim((string) $value);
if ($text === '') {
return self::now();
}
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $text)) {
return new DateTimeImmutable($text . ' 00:00:00', self::tz());
}
return (new DateTimeImmutable($text, self::tz()))->setTimezone(self::tz());
}
public static function toDbDate($value): ?string
{
if ($value === null || trim((string) $value) === '') {
return null;
}
return self::dateOnly($value)->format(self::DATE_LAYOUT);
}
public static function toDbDateTime($value): ?string
{
if ($value === null || trim((string) $value) === '') {
return null;
}
return self::toDateTime($value)->format(self::DATETIME_LAYOUT);
}
public static function formatRfc3339($value): string
{
if ($value === null || trim((string) $value) === '') {
return '';
}
return self::toDateTime($value)->format(DateTimeInterface::RFC3339);
}
public static function formatClock($value): string
{
if ($value === null || trim((string) $value) === '') {
return '';
}
return self::toDateTime($value)->format('H:i');
}
public static function parseHHMM(string $value): int
{
$text = trim($value);
if (!preg_match('/^\d{2}:\d{2}$/', $text)) {
throw new \RuntimeException('作息时间格式错误,应为 HH:MM', 400);
}
[$hour, $minute] = array_map('intval', explode(':', $text));
if ($hour < 0 || $hour > 23 || $minute < 0 || $minute > 59) {
throw new \RuntimeException('作息时间格式错误,应为 HH:MM', 400);
}
return $hour * 60 + $minute;
}
public static function awakeMinutes(string $wakeUpTime, string $sleepTime): int
{
$wake = trim($wakeUpTime);
$sleep = trim($sleepTime);
if ($wake === '' || $sleep === '') {
return 16 * 60;
}
$wakeMin = self::parseHHMM($wake);
$sleepMin = self::parseHHMM($sleep);
if ($wakeMin === $sleepMin) {
return 24 * 60;
}
if ($sleepMin > $wakeMin) {
return $sleepMin - $wakeMin;
}
return (24 * 60 - $wakeMin) + $sleepMin;
}
public static function baselineIntervalMinutes(int $awakeMinutes, int $baselineCigsPerDay): int
{
if ($awakeMinutes <= 0 || $baselineCigsPerDay <= 0) {
return 0;
}
return max(1, intdiv($awakeMinutes, $baselineCigsPerDay));
}
public static function normalizedMode(?string $mode): string
{
$value = trim((string) $mode);
return $value === 'quit' ? 'quit' : 'record';
}
public static function jsonArray($value): array
{
if (is_array($value)) {
return array_values(array_filter(array_map(function ($item) {
return trim((string) $item);
}, $value), static function ($item) {
return $item !== '';
}));
}
if ($value === null || $value === '') {
return [];
}
$decoded = json_decode((string) $value, true);
return is_array($decoded) ? self::jsonArray($decoded) : [];
}
public static function jsonEncodeArray(array $items): string
{
return json_encode(self::jsonArray($items), JSON_UNESCAPED_UNICODE);
}
public static function formatLog(array $row): array
{
return [
'id' => (int) ($row['id'] ?? 0),
'smoke_time' => self::formatRfc3339($row['smoke_time'] ?? null),
'smoke_at' => self::formatRfc3339($row['smoke_at'] ?? null),
'remark' => (string) ($row['remark'] ?? ''),
'reason_tags' => self::jsonArray($row['reason_tags'] ?? []),
'createtime' => isset($row['createtime']) ? (int) $row['createtime'] : 0,
'updatetime' => isset($row['updatetime']) ? (int) $row['updatetime'] : 0,
'deletetime' => isset($row['deletetime']) && $row['deletetime'] !== null ? (int) $row['deletetime'] : null,
'level' => (int) ($row['level'] ?? 1),
'num' => (int) ($row['num'] ?? 1),
];
}
public static function logEventAt(array $row): ?DateTimeImmutable
{
if (!empty($row['smoke_at'])) {
return self::toDateTime($row['smoke_at']);
}
if (!empty($row['createtime'])) {
return self::toDateTime((int) $row['createtime']);
}
if (!empty($row['smoke_time'])) {
return self::dateOnly($row['smoke_time']);
}
return null;
}
public static function weekRange(DateTimeImmutable $anchor): array
{
$weekday = (int) $anchor->format('N');
$start = self::dateOnly($anchor)->sub(new DateInterval('P' . ($weekday - 1) . 'D'));
return [$start, $start->add(new DateInterval('P6D'))];
}
public static function daysBetweenInclusive(DateTimeImmutable $start, DateTimeImmutable $end): int
{
$start = self::dateOnly($start);
$end = self::dateOnly($end);
if ($end < $start) {
return 0;
}
return (int) $start->diff($end)->days + 1;
}
public static function maskNickname(string $value): string
{
$text = trim($value);
if ($text === '') {
return '戒烟用户';
}
$length = mb_strlen($text);
if ($length <= 1) {
return '戒烟用户';
}
if ($length === 2) {
return mb_substr($text, 0, 1) . '*';
}
return mb_substr($text, 0, 1) . str_repeat('*', $length - 2) . mb_substr($text, -1);
}
public static function deriveUserSegment(int $baselineCigsPerDay, float $smokingYears): string
{
if ($baselineCigsPerDay >= 20 || $smokingYears >= 10) {
return 'heavy';
}
if ($baselineCigsPerDay >= 10 || $smokingYears >= 3) {
return 'moderate';
}
return 'newbie';
}
public static function truncate(string $value, int $max): string
{
if ($max <= 0 || mb_strlen($value) <= $max) {
return $value;
}
return mb_substr($value, 0, $max);
}
}