sendcard/api/getcard_poll.php
2026-04-14 09:47:00 +08:00

157 lines
5.1 KiB
PHP
Raw 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
declare(strict_types=1);
/**
* 定时轮询拉取外部凭证并追加写入本地 txt逻辑与 getcard 调同一 external 接口,仅触发节奏不同)。
*
* 设备:使用 config['poll'] 的机器 B 的 device_code / device_code_md5getcard.php 使用顶层机器 A。
* 原因:上游常对单设备或请求频次有限制,即时请求走 A、定时轮询走 B避免抢同一套额度。
*
* 间隔:基准 5 分钟 ±3 分钟随机,且不少于 4 分钟(由「下次执行时间」控制)。
* 宝塔 / Linux计划任务每分钟执行一次本脚本未到点会直接退出不会请求外部接口。
*
* 示例 crontab把路径改成你的站点根目录下的本文件
* * * * * * /usr/bin/php /www/wwwroot/你的域名/api/getcard_poll.php >>/www/wwwroot/你的域名/data/getcard_poll_cron.log 2>&1
*/
$cfg = require __DIR__ . '/../config.php';
$isCli = PHP_SAPI === 'cli';
$cronKey = isset($cfg['poll_cron_key']) ? (string)$cfg['poll_cron_key'] : '';
if (!$isCli) {
if ($cronKey === '' || (($_GET['key'] ?? '') !== $cronKey)) {
http_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
echo 'Forbidden';
exit;
}
header('Content-Type: text/plain; charset=utf-8');
}
$dataDir = __DIR__ . '/../data';
if (!is_dir($dataDir)) {
if (!@mkdir($dataDir, 0755, true) && !is_dir($dataDir)) {
sendcard_poll_out("Cannot create data directory: {$dataDir}\n");
exit(1);
}
}
$statePath = isset($cfg['poll_state_json']) && $cfg['poll_state_json'] !== ''
? (string)$cfg['poll_state_json']
: $dataDir . DIRECTORY_SEPARATOR . 'getcard_poll_state.json';
$logPath = isset($cfg['poll_log_txt']) && $cfg['poll_log_txt'] !== ''
? (string)$cfg['poll_log_txt']
: $dataDir . DIRECTORY_SEPARATOR . 'getcard_poll_log.txt';
$externalBase = trim((string)($cfg['external_base_url'] ?? ''));
if ($externalBase === '') {
sendcard_poll_out("Config missing: external_base_url\n");
exit(1);
}
try {
$externalUrl = sendcard_poll_build_external_url($cfg);
} catch (InvalidArgumentException $e) {
sendcard_poll_out('Config invalid: ' . $e->getMessage() . "\n");
exit(1);
}
$now = time();
$fh = fopen($statePath, 'c+');
if ($fh === false) {
sendcard_poll_out("Cannot open state file: {$statePath}\n");
exit(1);
}
if (!flock($fh, LOCK_EX)) {
fclose($fh);
sendcard_poll_out("Cannot lock state file\n");
exit(1);
}
$raw = stream_get_contents($fh);
$state = is_string($raw) && $raw !== '' ? json_decode($raw, true) : null;
if (!is_array($state)) {
$state = [];
}
$nextRunAt = isset($state['next_run_at']) ? (int)$state['next_run_at'] : 0;
if ($now < $nextRunAt) {
flock($fh, LOCK_UN);
fclose($fh);
$wait = $nextRunAt - $now;
sendcard_poll_out("Skip: next run in {$wait}s (at " . date('Y-m-d H:i:s', $nextRunAt) . ")\n");
exit(0);
}
require_once __DIR__ . '/../lib/external.php';
$linePrefix = '[' . date('Y-m-d H:i:s') . '] ';
$logBlock = '';
try {
$resp = sendcard_fetch_credentials($externalUrl);
$httpStatus = $resp['http_status'];
$json = $resp['json'];
$token = isset($json['data']['token']) ? (string)$json['data']['token'] : null;
$logBlock = $token !== null && $token !== ''
? $token . "\n"
: $linePrefix . "http={$httpStatus} token missing: " . json_encode($json, JSON_UNESCAPED_UNICODE) . "\n";
} catch (Throwable $e) {
$logBlock = $linePrefix . 'ERROR ' . $e->getMessage() . "\n";
}
// $intervalSec = max(4 * 60, 5 * 60 + random_int(-3 * 60, 3 * 60));
$intervalSec = random_int(540, 840);
$newNext = $now + $intervalSec;
$state['next_run_at'] = $newNext;
$state['last_run_at'] = $now;
$state['last_interval_sec'] = $intervalSec;
rewind($fh);
ftruncate($fh, 0);
fwrite($fh, json_encode($state, JSON_UNESCAPED_UNICODE));
fflush($fh);
flock($fh, LOCK_UN);
fclose($fh);
if ($logBlock !== '') {
file_put_contents($logPath, $logBlock, FILE_APPEND | LOCK_EX);
}
sendcard_poll_out(
$logBlock . "Scheduled next run in {$intervalSec}s (at " . date('Y-m-d H:i:s', $newNext) . ")\n"
);
exit(0);
function sendcard_poll_out(string $msg): void
{
echo $msg;
}
/**
* 轮询请求 URL使用 config['poll'] 下的 device_code、device_code_md5 作为查询参数值(与 getcard 顶层 device_* 分离)。
*
* @param array<string, mixed> $cfg
*/
function sendcard_poll_build_external_url(array $cfg): string
{
$base = trim((string)($cfg['external_base_url'] ?? ''));
if ($base === '') {
throw new InvalidArgumentException('external_base_url');
}
$poll = $cfg['poll'] ?? null;
if (!is_array($poll)) {
throw new InvalidArgumentException("config['poll'] 须为数组且含 device_code、device_code_md5");
}
$dc = trim((string)($poll['device_code'] ?? ''));
$dm = trim((string)($poll['device_code_md5'] ?? ''));
if ($dc === '' || $dm === '') {
throw new InvalidArgumentException("config['poll']['device_code'] 与 ['device_code_md5'] 勿留空");
}
return $base . '?device_code=' . rawurlencode($dc) . '&device_code_md5=' . rawurlencode($dm);
}