feat: 小程序与管理端迭代(神仙AI、了解自己CRM、AI测试入口、报表与分润等)
Made-with: Cursor
This commit is contained in:
128
api/app/common/service/AiBalanceAlertService.php
Normal file
128
api/app/common/service/AiBalanceAlertService.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
namespace app\common\service;
|
||||
|
||||
use app\model\AiProvider as AiProviderModel;
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* AI 服务商欠费预警
|
||||
*
|
||||
* 复用 `feishu_lead_webhook` 的 webhookUrl,但触发文案独立(不用走 lead 模板);
|
||||
* 每个服务商·每天·同阈值只推一次(ai_balance_alerts 表)。
|
||||
*
|
||||
* 触发建议:
|
||||
* 1) 超管页面手动点击「余额检查」
|
||||
* 2) 宝塔/Linux crontab 每天 9:00 + 21:00 POST 调用
|
||||
* curl -X POST -H "Authorization: Bearer {SUPERADMIN_TOKEN}" https://.../api/v1/superadmin/ai/balance-check
|
||||
*/
|
||||
class AiBalanceAlertService
|
||||
{
|
||||
/**
|
||||
* 扫描所有启用且开启预警的服务商,低于阈值则推飞书
|
||||
* @return array{alerted:int,skipped:int,items:array<int,array<string,mixed>>}
|
||||
*/
|
||||
public static function scanAndAlert(): array
|
||||
{
|
||||
$rows = AiProviderModel::where('enabled', 1)
|
||||
->where('balanceAlertEnabled', 1)
|
||||
->whereNotNull('lastBalance')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$alerted = 0;
|
||||
$skipped = 0;
|
||||
$items = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$threshold = (float) ($row['balanceAlertThreshold'] ?? 0);
|
||||
$balance = (float) $row['lastBalance'];
|
||||
if ($threshold <= 0 || $balance > $threshold) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dateStr = date('Y-m-d');
|
||||
$dup = Db::name('ai_balance_alerts')
|
||||
->where('providerId', $row['providerId'])
|
||||
->where('dateStr', $dateStr)
|
||||
->find();
|
||||
if ($dup) {
|
||||
$skipped++;
|
||||
$items[] = ['providerId' => $row['providerId'], 'status' => 'dedup'];
|
||||
continue;
|
||||
}
|
||||
|
||||
$ok = self::pushFeishu($row, $balance, $threshold);
|
||||
if ($ok) {
|
||||
try {
|
||||
Db::name('ai_balance_alerts')->insert([
|
||||
'providerId' => $row['providerId'],
|
||||
'balance' => $balance,
|
||||
'threshold' => $threshold,
|
||||
'currency' => $row['lastBalanceCurrency'] ?? 'CNY',
|
||||
'alertedAt' => time(),
|
||||
'dateStr' => $dateStr,
|
||||
]);
|
||||
} catch (\Throwable $e) {}
|
||||
$alerted++;
|
||||
$items[] = ['providerId' => $row['providerId'], 'status' => 'alerted', 'balance' => $balance, 'threshold' => $threshold];
|
||||
} else {
|
||||
$items[] = ['providerId' => $row['providerId'], 'status' => 'send-failed'];
|
||||
}
|
||||
}
|
||||
|
||||
return ['alerted' => $alerted, 'skipped' => $skipped, 'items' => $items];
|
||||
}
|
||||
|
||||
private static function pushFeishu(array $row, float $balance, float $threshold): bool
|
||||
{
|
||||
$cfg = FeishuLeadWebhookService::getConfig();
|
||||
$url = trim((string) ($cfg['webhookUrl'] ?? ''));
|
||||
if ($url === '' || stripos($url, 'http') !== 0) {
|
||||
Log::warning('AiBalanceAlertService: feishu webhookUrl 未配置');
|
||||
return false;
|
||||
}
|
||||
|
||||
$currency = ($row['lastBalanceCurrency'] ?? 'CNY') === 'USD' ? '$' : '¥';
|
||||
$now = date('Y-m-d H:i');
|
||||
$text = "⚠️ AI 服务余额预警\n"
|
||||
. "服务商: {$row['name']} ({$row['providerId']})\n"
|
||||
. "当前余额: {$currency}" . number_format($balance, 2) . "\n"
|
||||
. "告警阈值: {$currency}" . number_format($threshold, 2) . "\n"
|
||||
. "━━━━━━━━━━\n"
|
||||
. "神仙 AI 对话 / 面相分析可能受影响,已自动把该服务商降权到故障切换队尾。\n"
|
||||
. "请尽快到后台充值:\n"
|
||||
. "时间: {$now}";
|
||||
|
||||
$payload = stripos($url, 'qyapi.weixin.qq.com') !== false
|
||||
? ['msgtype' => 'text', 'text' => ['content' => $text]]
|
||||
: ['msg_type' => 'text', 'content' => ['text' => $text]];
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json; charset=utf-8'],
|
||||
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 8,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
]);
|
||||
$body = curl_exec($ch);
|
||||
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($code >= 400) {
|
||||
Log::warning("AiBalanceAlertService feishu push http={$code}");
|
||||
return false;
|
||||
}
|
||||
if ($body !== false && $body !== '') {
|
||||
$resp = json_decode($body, true);
|
||||
if (is_array($resp)) {
|
||||
if (isset($resp['code']) && (int) $resp['code'] !== 0) return false;
|
||||
if (isset($resp['StatusCode']) && (int) $resp['StatusCode'] !== 0) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user