feat: add smt module
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user