perf: V0.9.4 商品列表分页 + sitemap 文件缓存 + banner/asset/gzip/类型修复

- P1: Product::listing() 分页(WHERE status=1+LIMIT/OFFSET+总数),ProductController 改用并加翻页器,消除列表全表
- P1: outputSitemap() 加 storage/cache/sitemap.xml 文件缓存(TTL 3600s),消除每次爬虫全表
- P2: 首页 banner 改 whereLimit('status',1,20)
- P2: asset() 追加 ?v=filemtime 版本串,theme.css 改后自动击穿 1y 浏览器缓存
- P2: nginx 启用 gzip(文本资源压缩)
- P2: Model::count() $val 隐式可空改 mixed(PHP 8.4+ 弃用)
This commit is contained in:
2026-08-08 23:53:52 +08:00
parent 2285998618
commit d5c1edefae
8 changed files with 83 additions and 7 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ class HomeController extends Controller
'noindex' => $seo['noindex'],
'jsonld' => $faqJsonLd,
],
'banners' => array_filter($banner->all(), fn($b) => ($b['status'] ?? 1) == 1),
'banners' => $banner->whereLimit('status', 1, 20),
'categories'=> $category->all(),
'products' => $product->featured(8),
'news' => $news->published(3),
+9 -3
View File
@@ -10,11 +10,15 @@ class ProductController extends Controller
{
$product = new Product();
$category = new Category();
$perPage = 12;
$page = max(1, (int)($_GET['page'] ?? 1));
$catId = isset($_GET['cat']) ? (int)$_GET['cat'] : 0;
$products = $catId
? $product->byCategory($catId)
: array_filter($product->all(), fn($p) => ($p['status'] ?? 1) == 1);
// V0.9.4:列表改为分页查询(WHERE status=1 + LIMIT/OFFSET + 总数),不再全表拉回。
$res = $product->listing($page, $perPage, $catId ?: null);
$products = $res['items'];
$total = $res['total'];
$totalPages = max(1, (int) ceil($total / $perPage));
$cat = $catId ? $category->find($catId) : null;
$catName = $cat['name'] ?? '';
@@ -56,6 +60,8 @@ class ProductController extends Controller
'categories'=> $category->all(),
'activeCat' => $catId,
'cat' => $cat,
'page' => $page,
'totalPages'=> $totalPages,
]);
}
+15 -1
View File
@@ -328,9 +328,17 @@ class App
return $v;
}
/** 动态生成 Sitemap XMLGoogle/Bing/Baidu 自动抓取) */
/** 动态生成 Sitemap XMLGoogle/Bing/Baidu 自动抓取)。V0.9.4:加文件缓存(TTL),避免每次爬虫请求全表扫描。 */
private static function outputSitemap(): void
{
$cacheFile = rtrim(BASE_PATH, '/') . '/storage/cache/sitemap.xml';
$ttl = 3600;
if (is_file($cacheFile) && (time() - filemtime($cacheFile)) < $ttl) {
header('Content-Type: application/xml; charset=utf-8');
readfile($cacheFile);
exit;
}
ob_start();
header('Content-Type: application/xml; charset=utf-8');
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
@@ -402,6 +410,12 @@ class App
}
echo '</urlset>';
$xml = ob_get_clean();
// 写文件缓存(best-effort:目录不存在则创建,写失败不影响本次输出)
$dir = dirname($cacheFile);
if (!is_dir($dir)) @mkdir($dir, 0755, true);
if (is_dir($dir)) @file_put_contents($cacheFile, $xml);
echo $xml;
exit;
}
}
+8 -1
View File
@@ -24,7 +24,14 @@ if (!function_exists('site_url')) {
}
function asset(string $path = ''): string
{
return site_url('assets/' . ltrim($path, '/'));
$url = site_url('assets/' . ltrim($path, '/'));
// V0.9.4:追加文件 mtime 作为版本串,使 theme.css 等静态资源在「重新生成/部署」后自动击穿浏览器长缓存
//nginx 对已改资源设了 expires 1y,无版本串会导致老访客最长 1 年看不到更新)。
$real = rtrim(BASE_PATH, '/') . '/public/assets/' . ltrim($path, '/');
if (is_file($real)) {
$url .= '?v=' . filemtime($real);
}
return $url;
}
function e($v): string
{
+1 -1
View File
@@ -193,7 +193,7 @@ class Model
* @param string|null $col 过滤列(null 表示全表计数)
* @param mixed $val 过滤值
*/
public function count(?string $col = null, $val = null): int
public function count(?string $col = null, mixed $val = null): int
{
if (Db::driver() === 'mysql') {
if ($col === null) {
+28
View File
@@ -18,6 +18,34 @@ class Product extends Model
// 不再 SELECT * 全表拉回再 array_filter(原写法每次请求都全表扫描)。
return $this->whereLimit('status', 1, $limit);
}
/**
* V0.9.4:产品列表分页。仅取 status=1 的活跃商品,按 $orderBy 排序,带 LIMIT/OFFSET
* 同时返回总数用于翻页。避免每次列表请求都 SELECT * 全表拉回再 array_filter(原写法随 SKU 增长变重)。
* 文件模式回退为全表过滤 + array_slice。
* @return array{items:array,total:int}
*/
public function listing(int $page, int $perPage, ?int $catId = null): array
{
$page = max(1, $page);
$perPage = max(1, $perPage);
$offset = ($page - 1) * $perPage;
if (\Core\Db::driver() === 'mysql') {
$where = 'WHERE status = 1';
$args = [];
if ($catId !== null) { $where .= ' AND category_id = ?'; $args[] = $catId; }
$items = \Core\Db::query(
"SELECT * FROM `{$this->table}` {$where} ORDER BY `{$this->orderBy}` ASC LIMIT ? OFFSET ?",
array_merge($args, [$perPage, $offset])
)->fetchAll();
$total = (int) \Core\Db::query("SELECT COUNT(*) FROM `{$this->table}` {$where}", $args)->fetchColumn();
return ['items' => $items, 'total' => $total];
}
// 文件模式兜底
$all = array_filter($this->all(), fn($p) => ($p['status'] ?? 1) == 1);
if ($catId !== null) $all = array_filter($all, fn($p) => ($p['category_id'] ?? 0) == $catId);
$all = array_values($all);
return ['items' => array_slice($all, $offset, $perPage), 'total' => count($all)];
}
public function specsArray($p): array
{
$s = $p['specs'] ?? '';
+13
View File
@@ -44,5 +44,18 @@ $sub = $cat ? $cat['description'] : '水冷循环 / 相变蓄冷 / 涡扇风冷
</div>
<?php endforeach; ?>
</div>
<?php if (($totalPages ?? 1) > 1): ?>
<?php
$base = 'products' . ($activeCat ? '?cat=' . $activeCat : '');
$mk = fn($p) => site_url($base . ($activeCat ? '&' : '?') . 'page=' . $p);
?>
<nav class="pager" style="display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin:34px 0">
<?php if ($page > 1): ?><a class="btn btn-ghost" href="<?php echo $mk($page - 1); ?>">上一页</a><?php endif; ?>
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<a class="btn btn-ghost" style="<?php echo $i == $page ? 'background:var(--c-primary);color:#fff' : '' ?>" href="<?php echo $mk($i); ?>"><?php echo $i; ?></a>
<?php endfor; ?>
<?php if ($page < $totalPages): ?><a class="btn btn-ghost" href="<?php echo $mk($page + 1); ?>">下一页</a><?php endif; ?>
</nav>
<?php endif; ?>
</div>
</section>
+8
View File
@@ -24,6 +24,14 @@ server {
root /www/wwwroot/coolcoth.com/public; # 运行目录 = /public
index index.php index.html;
# ── Gzip 压缩(V0.9.4:文本资源压缩,降低传输体积)──
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 5;
gzip_types text/plain text/css text/xml application/xml application/javascript application/json application/x-javascript image/svg+xml;
# ── SSL 证书(宝塔申请 Let's Encrypt 后自动填充,或手动指定)──
# ssl_certificate /www/server/panel/vhost/cert/coolcoth.com/fullchain.pem;
# ssl_certificate_key /www/server/panel/vhost/cert/coolcoth.com/privkey.pem;