Files
nxxy335 94ac381579 完成批次 L-R:限流移除/白名单驱动/打印增强/全屏/表格对齐/主题变量/滚动闪烁修复
- 移除登录失败限流;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
2026-09-13 10:25:48 +08:00

107 lines
3.1 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
/**
* 数据结果缓存(短期 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);
}
}
}