Files
FendxPHP/fendx-framework/fendx-i18n/src/Timezone/Database/TimezoneDatabase.php
Lawson 2782d765fb feat(database): 添加用户角色权限系统及相关监控功能
- 创建用户表(users)包含基本信息和认证字段
- 创建角色表(roles)用于权限控制
- 创建权限表(permissions)定义系统权限
- 创建用户角色关联表(user_roles)建立用户与角色关系
- 创建角色权限关联表(role_permissions)建立角色与权限关系
- 创建迁移记录表(migrations)追踪数据库变更
- 添加AdminController提供管理员面板功能
- 实现系统监控、配置管理、缓存清理等功能
- 添加AOP切面编程支持的各种通知类型
- 实现告警管理AlertManager支持多渠道告警
- 添加文档注解接口规范
2026-04-08 17:00:28 +08:00

794 lines
22 KiB
PHP

<?php
declare(strict_types=1);
namespace Fendx\I18n\Timezone\Database;
use Fendx\I18n\Timezone\Database\Loader\TimezoneLoader;
use Fendx\I18n\Timezone\Database\Cache\TimezoneCache;
use Fendx\I18n\Timezone\Database\Index\TimezoneIndex;
class TimezoneDatabase
{
protected TimezoneLoader $loader;
protected TimezoneCache $cache;
protected TimezoneIndex $index;
protected array $config = [];
protected array $timezoneData = [];
protected array $locationData = [];
protected array $metadata = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->loader = new TimezoneLoader($this->config);
$this->cache = new TimezoneCache($this->config);
$this->index = new TimezoneIndex($this->config);
$this->initializeDatabase();
}
/**
* Get timezone information.
*/
public function getTimezoneInfo(string $timezone): array
{
if (!isset($this->timezoneData[$timezone])) {
$this->loadTimezoneData($timezone);
}
$data = $this->timezoneData[$timezone] ?? [];
if (empty($data)) {
throw new \InvalidArgumentException("Timezone not found: {$timezone}");
}
return $data;
}
/**
* Get timezone location information.
*/
public function getLocation(string $timezone): array
{
if (!isset($this->locationData[$timezone])) {
$this->loadLocationData($timezone);
}
return $this->locationData[$timezone] ?? [];
}
/**
* Get timezone metadata.
*/
public function getMetadata(string $timezone): array
{
if (!isset($this->metadata[$timezone])) {
$this->loadMetadata($timezone);
}
return $this->metadata[$timezone] ?? [];
}
/**
* Search timezones by criteria.
*/
public function search(array $criteria): array
{
return $this->index->search($criteria, $this->timezoneData);
}
/**
* Get timezones by country.
*/
public function getTimezonesByCountry(string $countryCode): array
{
return $this->search(['country' => $countryCode]);
}
/**
* Get timezones by region.
*/
public function getTimezonesByRegion(string $region): array
{
return $this->search(['region' => $region]);
}
/**
* Get timezones by city.
*/
public function getTimezonesByCity(string $city): array
{
return $this->search(['city' => $city]);
}
/**
* Get timezones by coordinates.
*/
public function getTimezonesByCoordinates(float $latitude, float $longitude, float $radius = 50): array
{
return $this->index->searchByCoordinates($latitude, $longitude, $radius, $this->timezoneData);
}
/**
* Get timezone by IP address.
*/
public function getTimezoneByIP(string $ipAddress): ?string
{
$location = $this->getLocationByIP($ipAddress);
if ($location) {
$timezones = $this->getTimezonesByCoordinates(
$location['latitude'],
$location['longitude'],
100
);
return !empty($timezones) ? $timezones[0]['timezone'] : null;
}
return null;
}
/**
* Get location by IP address.
*/
public function getLocationByIP(string $ipAddress): ?array
{
// This would integrate with a GeoIP service
// For now, return a basic implementation
return $this->loader->loadLocationByIP($ipAddress);
}
/**
* Get popular timezones.
*/
public function getPopularTimezones(int $limit = 20): array
{
$popularTimezones = $this->config['popular_timezones'] ?? [];
$result = [];
foreach ($popularTimezones as $timezone) {
if (isset($this->timezoneData[$timezone])) {
$result[] = $this->timezoneData[$timezone];
}
}
return array_slice($result, 0, $limit);
}
/**
* Get timezone offsets for all timezones at specific datetime.
*/
public function getAllOffsets(\DateTimeInterface $datetime = null): array
{
$datetime = $datetime ?? new \DateTime();
$offsets = [];
foreach ($this->timezoneData as $timezone => $data) {
try {
$tz = new \DateTimeZone($timezone);
$dt = new \DateTime($datetime->format('Y-m-d H:i:s'), $tz);
$offsets[$timezone] = [
'offset' => $tz->getOffset($dt),
'offset_hours' => $tz->getOffset($dt) / 3600,
'abbreviation' => $dt->format('T'),
'is_dst' => $dt->format('I') === '1'
];
} catch (\Exception $e) {
// Skip invalid timezones
continue;
}
}
return $offsets;
}
/**
* Get timezone conversion matrix.
*/
public function getConversionMatrix(array $timezones = null): array
{
$timezones = $timezones ?? array_keys($this->timezoneData);
$matrix = [];
$now = new \DateTime();
foreach ($timezones as $fromTz) {
$matrix[$fromTz] = [];
foreach ($timezones as $toTz) {
if ($fromTz === $toTz) {
$matrix[$fromTz][$toTz] = [
'offset_difference' => 0,
'offset_hours' => 0,
'time_difference' => '00:00:00'
];
} else {
$fromZone = new \DateTimeZone($fromTz);
$toZone = new \DateTimeZone($toTz);
$fromOffset = $fromZone->getOffset($now);
$toOffset = $toZone->getOffset($now);
$difference = $toOffset - $fromOffset;
$hours = floor(abs($difference) / 3600);
$minutes = floor((abs($difference) % 3600) / 60);
$sign = $difference >= 0 ? '+' : '-';
$matrix[$fromTz][$toTz] = [
'offset_difference' => $difference,
'offset_hours' => $difference / 3600,
'time_difference' => sprintf('%s%02d:%02d:00', $sign, $hours, $minutes)
];
}
}
}
return $matrix;
}
/**
* Get timezone groups.
*/
public function getTimezoneGroups(): array
{
return [
'americas' => $this->getTimezonesByRegion('America'),
'europe' => $this->getTimezonesByRegion('Europe'),
'africa' => $this->getTimezonesByRegion('Africa'),
'asia' => $this->getTimezonesByRegion('Asia'),
'australia' => $this->getTimezonesByRegion('Australia'),
'pacific' => $this->getTimezonesByRegion('Pacific'),
'antarctica' => $this->getTimezonesByRegion('Antarctica'),
'arctic' => $this->getTimezonesByRegion('Arctic'),
'indian' => $this->getTimezonesByRegion('Indian'),
'atlantic' => $this->getTimezonesByRegion('Atlantic')
];
}
/**
* Get timezone by abbreviation.
*/
public function getTimezoneByAbbreviation(string $abbreviation, \DateTimeInterface $datetime = null): array
{
$datetime = $datetime ?? new \DateTime();
$matches = [];
foreach ($this->timezoneData as $timezone => $data) {
try {
$tz = new \DateTimeZone($timezone);
$dt = new \DateTime($datetime->format('Y-m-d H:i:s'), $tz);
if ($dt->format('T') === $abbreviation) {
$matches[] = [
'timezone' => $timezone,
'offset' => $tz->getOffset($dt),
'is_dst' => $dt->format('I') === '1',
'data' => $data
];
}
} catch (\Exception $e) {
continue;
}
}
return $matches;
}
/**
* Get timezone statistics.
*/
public function getStatistics(): array
{
$stats = [
'total_timezones' => count($this->timezoneData),
'total_countries' => count($this->getCountries()),
'total_regions' => count($this->getRegions()),
'total_cities' => count($this->getCities()),
'dst_observing_timezones' => 0,
'non_dst_observing_timezones' => 0,
'offset_distribution' => [],
'region_distribution' => []
];
$now = new \DateTime();
foreach ($this->timezoneData as $timezone => $data) {
try {
$tz = new \DateTimeZone($timezone);
$dt = new \DateTime($now->format('Y-m-d H:i:s'), $tz);
if ($dt->format('I') === '1') {
$stats['dst_observing_timezones']++;
} else {
$stats['non_dst_observing_timezones']++;
}
$offset = $tz->getOffset($dt) / 3600;
$offsetKey = (string) $offset;
$stats['offset_distribution'][$offsetKey] = ($stats['offset_distribution'][$offsetKey] ?? 0) + 1;
$region = explode('/', $timezone)[0];
$stats['region_distribution'][$region] = ($stats['region_distribution'][$region] ?? 0) + 1;
} catch (\Exception $e) {
continue;
}
}
ksort($stats['offset_distribution']);
ksort($stats['region_distribution']);
return $stats;
}
/**
* Validate timezone data.
*/
public function validateTimezone(string $timezone): array
{
$errors = [];
$warnings = [];
try {
$tz = new \DateTimeZone($timezone);
$now = new \DateTime('now', $tz);
// Basic validation
if (!in_array($timezone, \DateTimeZone::listIdentifiers())) {
$errors[] = "Timezone not in PHP's timezone list";
}
// Check for data completeness
if (!isset($this->timezoneData[$timezone])) {
$warnings[] = "No extended data available for timezone";
}
// Check for location data
if (!isset($this->locationData[$timezone])) {
$warnings[] = "No location data available for timezone";
}
// Validate offset
$offset = $tz->getOffset($now);
if ($offset % 900 !== 0) { // Not aligned to 15-minute intervals
$warnings[] = "Timezone offset not aligned to 15-minute intervals";
}
} catch (\Exception $e) {
$errors[] = "Invalid timezone: " . $e->getMessage();
}
return [
'valid' => empty($errors),
'errors' => $errors,
'warnings' => $warnings
];
}
/**
* Update timezone data.
*/
public function updateTimezoneData(string $timezone, array $data): void
{
$this->timezoneData[$timezone] = array_merge(
$this->timezoneData[$timezone] ?? [],
$data
);
$this->index->updateIndex($timezone, $data);
if ($this->config['cache_enabled']) {
$this->cache->set($timezone, $this->timezoneData[$timezone]);
}
}
/**
* Add custom timezone.
*/
public function addCustomTimezone(string $timezone, array $data): void
{
if (isset($this->timezoneData[$timezone])) {
throw new \InvalidArgumentException("Timezone already exists: {$timezone}");
}
$this->updateTimezoneData($timezone, $data);
}
/**
* Remove timezone.
*/
public function removeTimezone(string $timezone): void
{
unset($this->timezoneData[$timezone]);
unset($this->locationData[$timezone]);
unset($this->metadata[$timezone]);
$this->index->removeFromIndex($timezone);
if ($this->config['cache_enabled']) {
$this->cache->delete($timezone);
}
}
/**
* Export timezone data.
*/
public function exportData(string $format = 'json'): string
{
$data = [
'timezone_data' => $this->timezoneData,
'location_data' => $this->locationData,
'metadata' => $this->metadata,
'statistics' => $this->getStatistics(),
'exported_at' => date('Y-m-d H:i:s'),
'version' => $this->config['data_version'] ?? '1.0'
];
switch ($format) {
case 'json':
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
case 'php':
return '<?php return ' . var_export($data, true) . ';';
case 'csv':
return $this->exportToCSV($data);
default:
throw new \InvalidArgumentException("Unsupported export format: {$format}");
}
}
/**
* Import timezone data.
*/
public function importData(string $data, string $format = 'json'): void
{
switch ($format) {
case 'json':
$imported = json_decode($data, true);
break;
case 'php':
$imported = include 'data://text/plain;base64,' . base64_encode($data);
break;
default:
throw new \InvalidArgumentException("Unsupported import format: {$format}");
}
if (!$imported) {
throw new \InvalidArgumentException("Invalid data format");
}
if (isset($imported['timezone_data'])) {
$this->timezoneData = array_merge($this->timezoneData, $imported['timezone_data']);
}
if (isset($imported['location_data'])) {
$this->locationData = array_merge($this->locationData, $imported['location_data']);
}
if (isset($imported['metadata'])) {
$this->metadata = array_merge($this->metadata, $imported['metadata']);
}
// Rebuild index
$this->index->rebuildIndex($this->timezoneData);
}
/**
* Get database version.
*/
public function getVersion(): string
{
return $this->config['data_version'] ?? '1.0';
}
/**
* Update database.
*/
public function updateDatabase(): void
{
$this->loader->updateDatabase();
$this->initializeDatabase();
}
/**
* Initialize database.
*/
protected function initializeDatabase(): void
{
// Load basic timezone data
$this->loadBasicTimezoneData();
// Build search index
$this->index->buildIndex($this->timezoneData);
// Warm up cache
if ($this->config['cache_enabled'] && $this->config['warmup_cache']) {
$this->warmUpCache();
}
}
/**
* Load basic timezone data.
*/
protected function loadBasicTimezoneData(): void
{
$identifiers = \DateTimeZone::listIdentifiers();
foreach ($identifiers as $timezone) {
$this->timezoneData[$timezone] = [
'timezone' => $timezone,
'identifier' => $timezone,
'region' => explode('/', $timezone)[0],
'city' => $this->extractCity($timezone),
'country' => $this->extractCountry($timezone)
];
}
}
/**
* Load timezone data.
*/
protected function loadTimezoneData(string $timezone): void
{
if ($this->config['cache_enabled']) {
$cached = $this->cache->get($timezone);
if ($cached) {
$this->timezoneData[$timezone] = $cached;
return;
}
}
$data = $this->loader->loadTimezoneData($timezone);
if ($data) {
$this->timezoneData[$timezone] = $data;
if ($this->config['cache_enabled']) {
$this->cache->set($timezone, $data);
}
}
}
/**
* Load location data.
*/
protected function loadLocationData(string $timezone): void
{
$data = $this->loader->loadLocationData($timezone);
if ($data) {
$this->locationData[$timezone] = $data;
}
}
/**
* Load metadata.
*/
protected function loadMetadata(string $timezone): void
{
$data = $this->loader->loadMetadata($timezone);
if ($data) {
$this->metadata[$timezone] = $data;
}
}
/**
* Extract city from timezone.
*/
protected function extractCity(string $timezone): string
{
$parts = explode('/', $timezone);
if (count($parts) >= 2) {
return str_replace('_', ' ', end($parts));
}
return $timezone;
}
/**
* Extract country from timezone.
*/
protected function extractCountry(string $timezone): string
{
// This is a simplified country extraction
// In practice, you'd use a more comprehensive mapping
$countryMap = [
'America' => 'US',
'Europe' => 'EU',
'Asia' => 'AS',
'Africa' => 'AF',
'Australia' => 'AU',
'Pacific' => 'OC'
];
$region = explode('/', $timezone)[0];
return $countryMap[$region] ?? 'Unknown';
}
/**
* Get countries.
*/
protected function getCountries(): array
{
$countries = [];
foreach ($this->timezoneData as $data) {
if (isset($data['country'])) {
$countries[$data['country']] = true;
}
}
return array_keys($countries);
}
/**
* Get regions.
*/
protected function getRegions(): array
{
$regions = [];
foreach ($this->timezoneData as $data) {
if (isset($data['region'])) {
$regions[$data['region']] = true;
}
}
return array_keys($regions);
}
/**
* Get cities.
*/
protected function getCities(): array
{
$cities = [];
foreach ($this->timezoneData as $data) {
if (isset($data['city'])) {
$cities[$data['city']] = true;
}
}
return array_keys($cities);
}
/**
* Export to CSV.
*/
protected function exportToCSV(array $data): string
{
$csv = "Timezone,Region,City,Country,Latitude,Longitude\n";
foreach ($data['timezone_data'] as $timezone => $info) {
$location = $data['location_data'][$timezone] ?? [];
$csv .= sprintf(
"%s,%s,%s,%s,%s,%s\n",
$timezone,
$info['region'] ?? '',
$info['city'] ?? '',
$info['country'] ?? '',
$location['latitude'] ?? '',
$location['longitude'] ?? ''
);
}
return $csv;
}
/**
* Warm up cache.
*/
protected function warmUpCache(): void
{
$popularTimezones = $this->config['popular_timezones'] ?? array_slice(array_keys($this->timezoneData), 0, 50);
foreach ($popularTimezones as $timezone) {
if (isset($this->timezoneData[$timezone])) {
$this->cache->set($timezone, $this->timezoneData[$timezone]);
}
}
}
/**
* Clear cache.
*/
public function clearCache(): void
{
$this->cache->clear();
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'cache_enabled' => true,
'cache_ttl' => 3600,
'warmup_cache' => true,
'data_version' => '1.0',
'popular_timezones' => [
'UTC',
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'Europe/London',
'Europe/Paris',
'Europe/Berlin',
'Asia/Shanghai',
'Asia/Tokyo',
'Asia/Hong_Kong',
'Asia/Singapore',
'Australia/Sydney',
'Pacific/Auckland'
],
'data_sources' => [
'tz_database' => true,
'geoip' => true,
'custom_data' => true
]
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create database instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'cache_enabled' => true,
'cache_ttl' => 7200,
'warmup_cache' => true,
'data_sources' => [
'tz_database' => true,
'geoip' => true,
'custom_data' => false
]
]);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'cache_enabled' => false,
'warmup_cache' => false,
'data_sources' => [
'tz_database' => true,
'geoip' => false,
'custom_data' => true
]
]);
}
}