- 移除登录失败限流;fix-config.php 修正 e2e 串味的 127.0.0.1 连接 - 白名单驱动缺/待考名单(DataService + SurveyKingClient::getWhitelist,复用已取 survey) - 打印成绩明细增强(范围/合格列默认关/去页脚);看板全屏占满;表格 vertical-align:middle;隐藏登陆账号;导入默认追加 - 主题变量收口(:root 新增 brand/primary-2/ok-2 等,替换硬编码渐变);删死规则 - 表头/数据列对齐修复(index 未匹配表、dashboard 成员榜、draw 结果/历史/排除表);看板占位卡片配色一致 - 修复滚动闪烁:row-drop-in 动画经 _dropAnim 门控,仅强制重绘时播放 - 测试:新增 test-whitelist.js;run-all.sh 全绿 132
107 lines
3.1 KiB
PHP
107 lines
3.1 KiB
PHP
<?php
|
||
/**
|
||
* 数据结果缓存(短期 TTL)
|
||
*
|
||
* 目的:拉取卷王全量答卷是重操作(多次 HTTP + 大量 json_decode),
|
||
* 多个老师同时查看同一套卷、或页面按 5~15 秒自动刷新时,会重复打卷王。
|
||
* 这里把 DataService::build() 的结果按「项目 + 配置 + 名单指纹」缓存一小段时间。
|
||
*
|
||
* 失效保证:名单变化(Roster::fingerprint)、连接/去重等配置变化都会改变 key。
|
||
* 另外可显式 force=1 跳过缓存(前端"立即刷新"按钮使用)。
|
||
*/
|
||
declare(strict_types=1);
|
||
|
||
final class DataCache
|
||
{
|
||
private static function dir(): string
|
||
{
|
||
return NY_DATA . '/cache';
|
||
}
|
||
|
||
/** 由若干要素拼成缓存 key */
|
||
public static function key(array $parts): string
|
||
{
|
||
return md5(implode("\x1f", array_map(static fn($v) => (string)$v, $parts)));
|
||
}
|
||
|
||
private static function path(string $key): string
|
||
{
|
||
return self::dir() . '/data_' . $key . '.json';
|
||
}
|
||
|
||
private static function lockPath(string $key): string
|
||
{
|
||
return self::dir() . '/data_' . $key . '.lock';
|
||
}
|
||
|
||
/**
|
||
* 读取缓存;不存在 / 过期 / 损坏都返回 null
|
||
* @return array{at:int,result:array}|null
|
||
*/
|
||
public static function read(string $key, int $ttl): ?array
|
||
{
|
||
$f = self::path($key);
|
||
if (!is_file($f)) {
|
||
return null;
|
||
}
|
||
$raw = @file_get_contents($f);
|
||
if ($raw === false || $raw === '') {
|
||
return null;
|
||
}
|
||
$d = json_decode($raw, true);
|
||
if (!is_array($d) || !isset($d['at']) || !is_array($d['result'])) {
|
||
return null;
|
||
}
|
||
if ($ttl > 0 && (time() - (int)$d['at']) > $ttl) {
|
||
return null;
|
||
}
|
||
return ['at' => (int)$d['at'], 'result' => $d['result']];
|
||
}
|
||
|
||
public static function write(string $key, array $result): bool
|
||
{
|
||
if (!is_dir(self::dir())) {
|
||
@mkdir(self::dir(), 0775, true);
|
||
}
|
||
$tmp = self::path($key) . '.tmp';
|
||
$ok = @file_put_contents($tmp, json_encode(
|
||
['at' => time(), 'result' => $result],
|
||
JSON_UNESCAPED_UNICODE
|
||
), LOCK_EX);
|
||
if ($ok === false) {
|
||
return false;
|
||
}
|
||
return @rename($tmp, self::path($key));
|
||
}
|
||
|
||
/**
|
||
* 获取构建锁(防止并发惊群:同一 key 只有一个请求真正去拉卷王)。
|
||
*
|
||
* @return resource|null 拿到锁返回句柄(需配合 releaseLock 释放);拿不到返回 null
|
||
*/
|
||
public static function acquireLock(string $key)
|
||
{
|
||
if (!is_dir(self::dir())) {
|
||
@mkdir(self::dir(), 0775, true);
|
||
}
|
||
$fh = @fopen(self::lockPath($key), 'c');
|
||
if ($fh === false) {
|
||
return null;
|
||
}
|
||
if (!flock($fh, LOCK_EX | LOCK_NB)) {
|
||
fclose($fh);
|
||
return null; // 已有其它请求在构建
|
||
}
|
||
return $fh;
|
||
}
|
||
|
||
public static function releaseLock($fh): void
|
||
{
|
||
if (is_resource($fh)) {
|
||
@flock($fh, LOCK_UN);
|
||
@fclose($fh);
|
||
}
|
||
}
|
||
|
||
}
|