Use Beijing business date for subscription delivery paths

This commit is contained in:
hellcat 2026-10-21 14:07:51 +08:00
parent d8aa76e7ad
commit 0a1698b69d
3 changed files with 98 additions and 2 deletions

View File

@ -8,6 +8,7 @@
use App\Services\TomTool\Telegram\Slave as TeleSlave;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\File;
use Carbon\CarbonImmutable;
class FreenodeController extends Controller
{
@ -22,7 +23,8 @@ public function filePut(Request $oRequest)
$sSubSiteCodeShort = $aData["site_code_short"];
$aRaw = $aData["raw"];
$sDate = date('Ymd');
// Subscription links use Beijing's calendar day, independently of app.timezone.
$sDate = CarbonImmutable::now('Asia/Shanghai')->format('Ymd');
$sSuffix = "nodes";
$sRawKey = $sDate . $sSuffix;
$sUniqueToken = hash_hmac('sha256', $sRawKey, 'wocaonide');
@ -47,7 +49,7 @@ public function filePut(Request $oRequest)
}
$sFileSavePath = $sSubSiteCodeShort."/".$sShortToken;
$sFileSaveName = date("Ymd")."-".$sClient.$sExtension;
$sFileSaveName = $sDate."-".$sClient.$sExtension;
$sFilePathFull = $sFileSavePath."/".$sFileSaveName;
if (!File::isDirectory($sFileSavePath)) {

View File

@ -0,0 +1,21 @@
# 免费节点订阅接收
Nodehub只负责接收和托管产物;上游生成、内核校验和站点分发见[master01维护正文](/Users/hellcat/Documents/GemCodes/dd.loc/docs/maintenance/FREENODE.md)。项目身份和访问规则见[GemDocs项目档案](/Users/hellcat/Documents/GemDocs/AI版/10-项目/nodehub.md)。
## 订阅日期契约
`POST /api/freenode/file/put`由`Api/v1/FreenodeController::filePut()`接收,参数仍为`site_code_short`和各格式`raw`,接口及链接算法不变。
接收时通过`CarbonImmutable::now('Asia/Shanghai')`确定一次业务日期,目录令牌和所有格式文件名均使用同一日期。应用默认时区仍为UTC,不改整个应用的日期、任务或日志。与master01按北京时间生成的当天链接保持一致;当天首次正常分发前不保证新日期文件已存在。
旧实现用UTC的`date()`,北京时间00:15、06:15两轮会写到前一天目录,直到12:15一轮才产生当天文件。2026-10-21已确认该问题;本次改动仅修复订阅接收日期,不移动历史文件、不复制旧节点伪造未来批次。
## 回归验证
`php vendor/bin/phpunit tests/Feature/FreenodeDateTest.php --do-not-cache-result`在隔离临时目录运行真实接收控制器,模拟北京时间零点前后以及00:15、06:15、12:15同步时刻,核对三种格式的日期、目录、内容以及全局UTC时区保持不变。Telegram被替换为测试对象,不访问网络或真实公开目录。
2026-10-21本机PHP 8.4.1通过5项、35断言。macOS本机使用真实PHP二进制`/usr/local/Cellar/php/8.4.1_1/bin/php`运行,避免Herd包装器导致PHPUnit隔离子进程启动失败。生产部署及验收结果另按实际记录。
## 发布与恢复
通过Git提交、服务端推送仓库,再将线上工作树更新到明确提交。提交只包括控制器、上述测试和本文;保留已有权限及未跟踪文件。部署前在服务器保存旧提交、控制器原字节/权限、工作区状态和当日订阅哈希;回退只还原本次控制器并记录代码版本,不reset整个有并行改动的工作树,不清除公开订阅目录。数据库、环境配置和系统时间不需要改动。

View File

@ -0,0 +1,73 @@
<?php
namespace Tests\Feature;
use App\Http\Controllers\Api\v1\FreenodeController;
use Carbon\CarbonImmutable;
use Illuminate\Http\Request;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\PreserveGlobalState;
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
use Tests\TestCase;
class FreenodeDateTest extends TestCase
{
public static function receivingTimes(): array
{
return [
'before Beijing midnight' => ['2026-10-21T15:59:59Z', '20261021'],
'Beijing midnight' => ['2026-10-21T16:00:00Z', '20261022'],
'midnight scheduled delivery' => ['2026-10-21T16:15:00Z', '20261022'],
'morning scheduled delivery' => ['2026-10-21T22:15:00Z', '20261022'],
'noon scheduled delivery' => ['2026-10-22T04:15:00Z', '20261022'],
];
}
#[DataProvider('receivingTimes')]
#[RunInSeparateProcess]
#[PreserveGlobalState(false)]
public function testReceiverWritesAllFormatsUnderBeijingDate(string $instant, string $date): void
{
$previousDirectory = getcwd();
$previousTimezone = date_default_timezone_get();
$directory = sys_get_temp_dir().'/nodehub-date-test-'.bin2hex(random_bytes(6));
mkdir($directory, 0700);
// Keep the real controller and filesystem, but never send Telegram in a test.
$telegram = Mockery::mock('alias:App\Services\TomTool\Telegram\Slave');
$telegram->shouldReceive('log')->once()->andReturnSelf();
$telegram->shouldReceive('enableDetail')->with(false)->once()->andReturnSelf();
$telegram->shouldReceive('send')->once();
try {
config(['app.timezone' => 'UTC', 'app.is_loc' => true]);
date_default_timezone_set('UTC');
CarbonImmutable::setTestNow(new CarbonImmutable($instant));
chdir($directory);
$raw = ['v2ray' => 'fixture-uri', 'clash' => 'fixture-clash', 'singbox' => 'fixture-singbox'];
$request = Request::create('/api/freenode/file/put', 'POST', ['site_code_short' => 'cfn', 'raw' => $raw]);
$response = (new FreenodeController())->filePut($request);
self::assertTrue($response->getData(true)['isDone']);
$token = substr(hash_hmac('sha256', $date.'nodes', 'wocaonide'), 0, 16);
foreach ($raw as $format => $content) {
$extension = $format === 'clash' ? '.yaml' : '.txt';
self::assertSame($content, file_get_contents($directory.'/cfn/'.$token.'/'.$date.'-'.$format.$extension));
}
self::assertCount(3, glob($directory.'/cfn/*/*'));
self::assertSame('UTC', date_default_timezone_get());
self::assertSame('UTC', config('app.timezone'));
} finally {
CarbonImmutable::setTestNow();
date_default_timezone_set($previousTimezone);
chdir($previousDirectory);
$files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file) {
$file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname());
}
rmdir($directory);
Mockery::close();
}
}
}