wpmind/modules/cost-control/includes/BudgetChecker.php
LinuxJoy 74db7a20af refactor: 方法名 camelCase → snake_case (39文件, WordPress 编码规范)
将项目自有代码的方法名从 camelCase 统一为 snake_case,
符合 WordPress PHP 编码规范。涉及 Routing、Failover、Budget、
Analytics、Usage、ErrorHandler 等模块。外部库接口方法和
Image Provider 子系统保持不变。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 03:21:31 +08:00

460 lines
13 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Budget Checker - 预算检查器
*
* 检查当前费用是否超出预算限额
*
* @package WPMind\Modules\CostControl
* @since 1.0.0
*/
declare(strict_types=1);
namespace WPMind\Modules\CostControl;
class BudgetChecker
{
/**
* 检查结果常量
*/
public const STATUS_OK = 'ok';
public const STATUS_WARNING = 'warning';
public const STATUS_EXCEEDED = 'exceeded';
/**
* 单例实例
*/
private static ?BudgetChecker $instance = null;
/**
* 告警状态缓存
*/
private const ALERT_TRANSIENT_KEY = 'wpmind_budget_alerts_sent';
/**
* 获取单例实例
*/
public static function instance(): BudgetChecker
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 私有构造函数
*/
private function __construct() {}
/**
* 检查全局预算状态
*
* @return array ['status' => string, 'details' => array]
*/
public function check_global_budget(): array
{
$manager = BudgetManager::instance();
if (!$manager->is_enabled() || !$manager->has_any_limits()) {
return ['status' => self::STATUS_OK, 'details' => []];
}
$global = $manager->get_global_budget();
$threshold = $manager->get_alert_threshold();
// 获取当前用量
$today = UsageTracker::get_today_stats();
$month = UsageTracker::get_month_stats();
$details = [];
$status = self::STATUS_OK;
// 检查每日 USD 限额
if (($global['daily_limit_usd'] ?? 0) > 0) {
$result = $this->check_limit(
$today['cost_usd'] ?? 0,
$global['daily_limit_usd'],
$threshold,
'daily_usd'
);
$details['daily_usd'] = $result;
$status = $this->merge_status($status, $result['status']);
}
// 检查每日 CNY 限额
if (($global['daily_limit_cny'] ?? 0) > 0) {
$result = $this->check_limit(
$today['cost_cny'] ?? 0,
$global['daily_limit_cny'],
$threshold,
'daily_cny'
);
$details['daily_cny'] = $result;
$status = $this->merge_status($status, $result['status']);
}
// 检查每月 USD 限额
if (($global['monthly_limit_usd'] ?? 0) > 0) {
$result = $this->check_limit(
$month['cost_usd'] ?? 0,
$global['monthly_limit_usd'],
$threshold,
'monthly_usd'
);
$details['monthly_usd'] = $result;
$status = $this->merge_status($status, $result['status']);
}
// 检查每月 CNY 限额
if (($global['monthly_limit_cny'] ?? 0) > 0) {
$result = $this->check_limit(
$month['cost_cny'] ?? 0,
$global['monthly_limit_cny'],
$threshold,
'monthly_cny'
);
$details['monthly_cny'] = $result;
$status = $this->merge_status($status, $result['status']);
}
return ['status' => $status, 'details' => $details];
}
/**
* 检查服务商预算状态
*
* @param string $provider 服务商 ID
* @return array ['status' => string, 'details' => array]
*/
public function check_provider_budget(string $provider): array
{
$manager = BudgetManager::instance();
if (!$manager->is_enabled()) {
return ['status' => self::STATUS_OK, 'details' => []];
}
$providerBudget = $manager->get_provider_budget($provider);
if (!$providerBudget) {
return ['status' => self::STATUS_OK, 'details' => []];
}
$threshold = $manager->get_alert_threshold();
// 获取服务商用量
$stats = UsageTracker::get_stats();
$providerStats = $stats['providers'][$provider] ?? null;
if (!$providerStats) {
return ['status' => self::STATUS_OK, 'details' => []];
}
$details = [];
$status = self::STATUS_OK;
// 获取服务商货币类型
$currency = UsageTracker::get_currency($provider);
// 检查每日限额
if (!empty($providerBudget['daily_limit']) && $providerBudget['daily_limit'] > 0) {
$todayCost = $this->get_provider_today_cost($provider);
$result = $this->check_limit(
$todayCost,
$providerBudget['daily_limit'],
$threshold,
"provider_{$provider}_daily"
);
$details['daily'] = $result;
$status = $this->merge_status($status, $result['status']);
}
// 检查每月限额
if (!empty($providerBudget['monthly_limit']) && $providerBudget['monthly_limit'] > 0) {
$monthCost = $this->get_provider_month_cost($provider);
$result = $this->check_limit(
$monthCost,
$providerBudget['monthly_limit'],
$threshold,
"provider_{$provider}_monthly"
);
$details['monthly'] = $result;
$status = $this->merge_status($status, $result['status']);
}
return ['status' => $status, 'details' => $details, 'currency' => $currency];
}
/**
* 检查是否允许发起请求
*
* @param string|null $provider 服务商 ID可选
* @return array ['allowed' => bool, 'reason' => string|null, 'status' => array]
*/
public function can_make_request(?string $provider = null): array
{
$manager = BudgetManager::instance();
if (!$manager->is_enabled()) {
return ['allowed' => true, 'reason' => null, 'status' => []];
}
$mode = $manager->get_enforcement_mode();
// 仅告警模式始终允许请求
if ($mode === BudgetManager::MODE_ALERT) {
return ['allowed' => true, 'reason' => null, 'status' => $this->check_global_budget()];
}
// 检查全局预算
$globalCheck = $this->check_global_budget();
if ($globalCheck['status'] === self::STATUS_EXCEEDED) {
return [
'allowed' => false,
'reason' => __('已超出全局预算限额', 'wpmind'),
'status' => $globalCheck,
];
}
// 检查服务商预算
if ($provider) {
$providerCheck = $this->check_provider_budget($provider);
if ($providerCheck['status'] === self::STATUS_EXCEEDED) {
return [
'allowed' => false,
'reason' => sprintf(__('已超出 %s 的预算限额', 'wpmind'), $provider),
'status' => $providerCheck,
];
}
}
return ['allowed' => true, 'reason' => null, 'status' => $globalCheck];
}
/**
* 获取所有服务商的预算状态
*
* @return array
*/
public function get_all_provider_status(): array
{
$manager = BudgetManager::instance();
$settings = $manager->get_settings();
$result = [];
foreach (($settings['providers'] ?? []) as $provider => $limits) {
$result[$provider] = $this->check_provider_budget($provider);
}
return $result;
}
/**
* 获取预算摘要(用于 UI 显示)
*
* @return array
*/
public function get_summary(): array
{
$manager = BudgetManager::instance();
if (!$manager->is_enabled()) {
return [
'enabled' => false,
'global' => null,
'providers' => [],
];
}
$global = $this->check_global_budget();
$providers = $this->get_all_provider_status();
// 计算使用百分比
$globalBudget = $manager->get_global_budget();
$today = UsageTracker::get_today_stats();
$month = UsageTracker::get_month_stats();
$percentages = [];
if (($globalBudget['daily_limit_usd'] ?? 0) > 0) {
$percentages['daily_usd'] = min(100, round(
(($today['cost_usd'] ?? 0) / $globalBudget['daily_limit_usd']) * 100,
1
));
}
if (($globalBudget['daily_limit_cny'] ?? 0) > 0) {
$percentages['daily_cny'] = min(100, round(
(($today['cost_cny'] ?? 0) / $globalBudget['daily_limit_cny']) * 100,
1
));
}
if (($globalBudget['monthly_limit_usd'] ?? 0) > 0) {
$percentages['monthly_usd'] = min(100, round(
(($month['cost_usd'] ?? 0) / $globalBudget['monthly_limit_usd']) * 100,
1
));
}
if (($globalBudget['monthly_limit_cny'] ?? 0) > 0) {
$percentages['monthly_cny'] = min(100, round(
(($month['cost_cny'] ?? 0) / $globalBudget['monthly_limit_cny']) * 100,
1
));
}
return [
'enabled' => true,
'global' => $global,
'providers' => $providers,
'percentages' => $percentages,
'threshold' => $manager->get_alert_threshold(),
'mode' => $manager->get_enforcement_mode(),
];
}
/**
* 检查单个限额
*/
private function check_limit(float $current, float $limit, int $threshold, string $key): array
{
if ($limit <= 0) {
return [
'status' => self::STATUS_OK,
'current' => $current,
'limit' => $limit,
'percentage' => 0,
];
}
$percentage = ($current / $limit) * 100;
if ($percentage >= 100) {
$status = self::STATUS_EXCEEDED;
} elseif ($percentage >= $threshold) {
$status = self::STATUS_WARNING;
} else {
$status = self::STATUS_OK;
}
return [
'status' => $status,
'current' => $current,
'limit' => $limit,
'percentage' => round($percentage, 1),
];
}
/**
* 合并状态(取最严重的)
*/
private function merge_status(string $current, string $new): string
{
$priority = [
self::STATUS_OK => 0,
self::STATUS_WARNING => 1,
self::STATUS_EXCEEDED => 2,
];
return ($priority[$new] ?? 0) > ($priority[$current] ?? 0) ? $new : $current;
}
/**
* 获取服务商今日费用
*/
private function get_provider_today_cost(string $provider): float
{
$history = UsageTracker::get_history(1000);
$today = wp_date('Y-m-d');
$todayStart = strtotime($today);
$cost = 0;
foreach ($history as $record) {
if (($record['provider'] ?? '') !== $provider) {
continue;
}
if (($record['timestamp'] ?? 0) >= $todayStart) {
$cost += $record['cost'] ?? 0;
}
}
return $cost;
}
/**
* 获取服务商本月费用
*/
private function get_provider_month_cost(string $provider): float
{
$stats = UsageTracker::get_stats();
return $stats['providers'][$provider]['total_cost'] ?? 0;
}
/**
* 检查是否需要发送告警
*/
public function should_send_alert(string $alertKey): bool
{
$sent = get_transient(self::ALERT_TRANSIENT_KEY);
if (!is_array($sent)) {
$sent = [];
}
$today = wp_date('Y-m-d');
$key = "{$alertKey}_{$today}";
return !isset($sent[$key]);
}
/**
* 标记告警已发送
*/
public function mark_alert_sent(string $alertKey): void
{
$sent = get_transient(self::ALERT_TRANSIENT_KEY);
if (!is_array($sent)) {
$sent = [];
}
$today = wp_date('Y-m-d');
$key = "{$alertKey}_{$today}";
$sent[$key] = time();
// 清理过期的告警记录
$yesterday = wp_date('Y-m-d', strtotime('-1 day'));
foreach ($sent as $k => $v) {
if (!str_contains($k, $today) && !str_contains($k, $yesterday)) {
unset($sent[$k]);
}
}
set_transient(self::ALERT_TRANSIENT_KEY, $sent, DAY_IN_SECONDS * 2);
}
/**
* 获取状态的显示标签
*/
public static function get_status_label(string $status): string
{
$labels = [
self::STATUS_OK => __('正常', 'wpmind'),
self::STATUS_WARNING => __('接近限额', 'wpmind'),
self::STATUS_EXCEEDED => __('已超限', 'wpmind'),
];
return $labels[$status] ?? $status;
}
/**
* 获取状态的 CSS 类
*/
public static function get_status_class(string $status): string
{
$classes = [
self::STATUS_OK => 'wpmind-budget-ok',
self::STATUS_WARNING => 'wpmind-budget-warning',
self::STATUS_EXCEEDED => 'wpmind-budget-exceeded',
];
return $classes[$status] ?? '';
}
}