苹果 CMS(MacCMS v10)的“页面缓存”是“整页缓存”,跟用户无关,同一个 URL 命中缓存后直接把整段 HTML 原样回显,不再走模板渲染、更不会重新计算 $user
。代码就写在 application/common/controller/All.php
的 load_page_cache / label_fetch
里,缓存键只由域名、移动端标记、缓存标记、模板名、URL 参数拼出来,没有用户维度;命中后直接 echo $res; die;
。所以你登录后仍有页面显示“未登录”,根因就是被“访客首个访问时”的页面缓存顶住了。源码可核对:$cach_name = $_SERVER['HTTP_HOST'].'_'.MAC_MOB.'_'.$GLOBALS['config']['app']['cache_flag'].'_'.$tpl.'_'.http_build_query(mac_param_url());
(命中即输出并终止)。GitHub
另外,GitHub文档也写过:模板里 $user
仅在非静态/非整页缓存渲染时可用;一旦整页缓存接管,就不会再重新赋值用户信息。GitHub
由于整页缓存是“同 URL 共用一份 HTML”,任何会显示个人信息(昵称/头像/积分)的位置不能参与整页缓存,否则要么所有人都看到“未登录”,要么所有登录用户看到“第一个造出缓存的那位用户”的姓名头像。
所以,要在“开启页面缓存”的前提下保证登录状态统一(最稳、推荐):整页缓存 + 动态碎片(AJAX/接口),保留页面缓存(性能最佳),把“头部/侧边用户块”改为异步加载。
实现方法(关键点):
- 新增接口(
PublicController@userInfo
),在方法开头加强制不缓存头:mac_no_cahche();
并返回public/user_info.html
片段(模板里可安全使用$GLOBALS['user']
)。MacCMS 在其他位置也用这个函数来禁止缓存(例如搜索风控),可以直接复用。GitHub - 头部和侧边把原有用户区块替换成一个空容器 + jQuery
$.get()
动态填充。 - 给接口加响应头
Vary: Cookie, Accept
(mac_no_cahche()
会做好 no-cache,Vary 手动加一行header('Vary: Cookie, Accept');
更稳),避免 CDN 误缓存。
优点:不泄露个人信息、不膨胀缓存体积;命中整页缓存的同时,用户块始终正确。
以下实现步骤及核心代码供参考。
① 控制器:新增 Ajax 用户信息碎片接口
文件路径:application/index/controller/Ajax.php
说明:
- 这里用
Ajax
控制器承载碎片接口,路由生成用{:url('ajax/userInfo')}
。- 强制加防缓存响应头和
Vary: Cookie, Accept
,避免 CDN/反代串号。- 用
$this->label_user()
准备$GLOBALS['user']
,渲染碎片模板public/user_info.html
。
/**
* 用户信息碎片(用于整页缓存下的异步用户区)
* URL: {:url('ajax/userInfo')}
*/
public function userInfo()
{
// 准备当前登录态(让 $GLOBALS['user'] 可用)
$this->label_user();
// —— 强制禁缓存(服务器/代理/CDN)+ 基于 Cookie 的变体隔离 ——
header('Content-Type: text/html; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, private');
header('Pragma: no-cache');
header('Expires: 0');
header('Vary: Cookie, Accept');
// 渲染并返回碎片模板(只包含用户区块)
return $this->fetch('public/user_info');
}
② 碎片模板:用户信息区(登录/未登录)
文件路径:application/index/view/public/user_info.html
说明:
- 与模板原顶部/侧边用户区 DOM/class 完全对齐。
- 未登录时只输出文字按钮,不输出 img,杜绝误判为“多一个头像位”。
- 登录后输出
member_group
+ 头像 + 下拉菜单。- PC/大屏:显示“登录”文字按钮(class: login-text)
- 移动小屏:显示公共用户头像(class: login-avatar,使用 .icon-yonghu-o + .icon20)
{if condition="$maccms.user_status eq 1"}
{if condition="$GLOBALS['user']['user_id'] gt 0"}
<!-- 已登录:显示头像 + 下拉菜单 -->
<div class="member_group" data-uid="{$GLOBALS['user']['user_id']}">
<img src="{$GLOBALS['user']['user_id']|mac_get_user_portrait}" class="useimg" alt="{$GLOBALS['user']['user_name']|default='用户'}">
<div class="user_list_drop">
<div class="drop_content">
<ul>
<li><a href="{:mac_url('user/index')}"><i class="icon icon-yh"></i>个人主页</a></li>
<li><a href="{:mac_url('user/info')}"><i class="icon icon-sz"></i>帐号设置</a></li>
<li><a href="{:mac_url('user/favs')}"><i class="icon icon-score"></i>我的收藏</a></li>
<!-- <li><a href="{:mac_url('user/upgrade')}"><i class="icon icon-vip"></i>升级会员</a></li> -->
</ul>
<div class="logout">
<a href="{:mac_url('user/logout')}"><i class="icon icon-exit"></i><span>退出登录</span></a>
</div>
</div>
</div>
</div>
{else/}
<!-- 未登录:
- PC/大屏:显示“登录”文字按钮(class: login-text)
- 移动小屏:显示公共用户头像(class: login-avatar,使用 .icon-yonghu-o + .icon20)
两个元素都带 .header-op-user,沿用现有的事件委托打开登录弹窗
-->
<div class="mac_user">
<div class="header-op-user login-text" title="会员中心">登录</div>
<div class="header-op-user login-avatar" aria-label="登录">
<i class="icon icon-yonghu-o icon20"></i>
</div>
</div>
{/if}
{/if}
头部模板:接入占位容器 + 注入脚本
文件路径:你的头部模板文件(例如 application/index/view/public/head.html
)。
操作:可能并不适用于你的模板,仅供参考。
说明:
- 顶部与侧边用户区各放一个占位容器:
#user-info-top
、#user-info-side
。- 注入脚本中:
- 移除骨架类(解决“多出一个头像位”的问题)
- 事件委托绑定:对
.header-op-user, .mac_user
委托点击 → 保证异步插入后仍能打开登录- 登录弹窗调用顺序:优先
MAC.User.Login()
(苹果CMS内置),其次window.showLoginPopup()
,最后兜底跳转到登录页- 保留了原有的搜索框、侧边栏、滚动样式与脚本
<!-- ============ 头部(Header) ============ -->
<div class="header">
<div class="header-box">
<div class="logo {if condition="$mxprost['mxprocms']['s2']['navtheme'] eq 1"}nonenav{/if}">
<a href="{$maccms.path}" title="{$maccms.site_name}">
<img class="logo2" src="{:mac_url_img($mxprost.mxprocms.s1.logo2)}" alt="{$maccms.site_name}">
<img class="logo1" src="{:mac_url_img($mxprost.mxprocms.s1.logo1)}">
</a>
</div>
<div class="search-box {if condition="$mxprost['mxprocms']['s2']['navtheme'] eq 1"}nonenav{/if}">
<div class="searchbar-main">
<form name="search" method="get" action="{:mac_url('vod/search')}" onSubmit="return qrsearch();">
<div class="searchbar">
<input class="search-input" type="text" name="wd" autocomplete="off" placeholder="{$mxprost.mxprocms.s1.searchwd}">
<button class="search-btn search-go" id="searchbutton" type="submit"><i class="icon-search"></i></button>
<button class="cancel-btn" type="button">取消</button>
</div>
<div class="search-recommend-box">
<div class="search-recommend">
<div class="search-recommend-title"><strong>大家都在看</strong></div>
<div class="search-tag">
{maccms:vod num="20" order="desc" by="hits_week" id="vo" key="key"}
<a href="{:mac_url_vod_detail($vo)}" class="{if condition='$key lt 4'}hot{/if}">
<i class="icon-hot"></i>{$vo.vod_name}
</a>
{/maccms:vod}
</div>
</div>
</div>
</form>
</div>
</div>
<div class="header-op">
<div class="header-op-list">
<div class="drop">
<div class="header-op-list-btn header-op-history"><i class="icon icon-history-o"></i><span>观看记录</span></div>
<div class="drop-content drop-history">
<div class="drop-content-box">
<ul class="drop-content-items historical">
<li class="drop-item drop-item-title"><i class="icon icon-history"></i><strong>我的观影记录</strong></li>
</ul>
</div>
</div>
<div class="shortcuts-mobile-overlay"></div>
</div>
<!-- 顶部用户区:异步碎片占位(由 ajax/userInfo 注入) -->
<div id="user-info-top" class="user-info-skeleton" aria-live="polite"></div>
<div class="header-op-list-btn header-op-search"><i class="icon icon-search"></i></div>
</div>
</div>
</div>
</div>
<!-- ============ 侧边(Sidebar) ============ -->
<div class="sidebar">
<div class="navbar {if condition="$mxprost['mxprocms']['s2']['navtheme'] eq 1"}open{/if}">
<ul class="navbar-items swiper-wrapper">
<li class="swiper-slide navbar-item {if condition="$maccms.aid eq 1"}active{/if}">
<a href="{$maccms.path}" class="links">
<div {if condition="$maccms.aid eq 1"}class="current"{/if}></div>
<i class="icon-arrow-go"></i><i class="icon icon-home-o"></i><span>首页</span>
</a>
</li>
<li class="navbar-hr"></li>
{maccms:type order="asc" by="sort" ids="'.$mxprost['mxprocms']['s2']['daohangid'].'" flag="vod" }
<li class="swiper-slide navbar-item {if condition="($vo.type_id eq $GLOBALS['type_id'] || $vo.type_id eq $GLOBALS['type_pid'])"}active{/if}">
<a href="{:mac_url_type($vo)}" title="{$vo.type_name}" class="links">
{if condition="($vo.type_id eq $GLOBALS['type_id'] || $vo.type_id eq $GLOBALS['type_pid'])"}<div class="current"></div>{/if}
<i class="icon-arrow-go"></i>
{if condition="$vo.type_id_1 eq $mxprost.mxprocms.s2.num1||$vo.type_id eq $mxprost.mxprocms.s2.num1"}
<i class="{$mxprost.mxprocms.s2.icon1}"></i>
{elseif condition="$vo.type_id_1 eq $mxprost.mxprocms.s2.num2||$vo.type_id eq $mxprost.mxprocms.s2.num2"}
<i class="{$mxprost.mxprocms.s2.icon2}"></i>
{elseif condition="$vo.type_id_1 eq $mxprost.mxprocms.s2.num3||$vo.type_id eq $mxprost.mxprocms.s2.num3"}
<i class="{$mxprost.mxprocms.s2.icon3}"></i>
{elseif condition="$vo.type_id_1 eq $mxprost.mxprocms.s2.num4||$vo.type_id eq $mxprost.mxprocms.s2.num4"}
<i class="{$mxprost.mxprocms.s2.icon4}"></i>
{elseif condition="$vo.type_id_1 eq $mxprost.mxprocms.s2.num5||$vo.type_id eq $mxprost.mxprocms.s2.num5"}
<i class="{$mxprost.mxprocms.s2.icon5}"></i>
{elseif condition="$vo.type_id_1 eq $mxprost.mxprocms.s2.num6||$vo.type_id eq $mxprost.mxprocms.s2.num6"}
<i class="{$mxprost.mxprocms.s2.icon6}"></i>
{elseif condition="$vo.type_id_1 eq $mxprost.mxprocms.s2.num7||$vo.type_id eq $mxprost.mxprocms.s2.num7"}
<i class="{$mxprost.mxprocms.s2.icon7}"></i>
{else}
<i class="icon-jl-o"></i>
{/if}
<span>{$vo.type_name}</span>
</a>
</li>
{/maccms:type}
<li class="navbar-hr"></li>
{if condition="$mxprost['mxprocms']['s2']['diy1'] eq 1"}
<li class="swiper-slide navbar-item">
<a href="{$mxprost.mxprocms.s2.diy1url}" class="links" title="{$mxprost.mxprocms.s2.diy1name}">
<i class="icon-arrow-go"></i><i class="icon {$mxprost.mxprocms.s2.diy1icon}"></i><span>{$mxprost.mxprocms.s2.diy1name}</span>
</a>
</li>
{/if}
{if condition="$mxprost['mxprocms']['s2']['diy2'] eq 1"}
<li class="swiper-slide navbar-item">
<a href="{$mxprost.mxprocms.s2.diy2url}" class="links" title="{$mxprost.mxprocms.s2.diy2name}">
<i class="icon-arrow-go"></i><i class="icon {$mxprost.mxprocms.s2.diy2icon}"></i><span>{$mxprost.mxprocms.s2.diy2name}</span>
</a>
</li>
{/if}
<li class="navbar-hr"></li>
{if condition="$mxprost['mxprocms']['s2']['navhot'] eq 1"}
<li class="swiper-slide navbar-item {if condition="$maccms.aid eq 9998"}active{/if}">
<a href="{:mac_url('label/hot')}" class="links">
{if condition="$maccms.aid eq 9998"}<div class="current"></div>{/if}
<i class="icon-arrow-go"></i><i class="icon icon-ranking-o"></i><span>热播</span>
</a>
</li>
{/if}
{if condition="$mxprost['mxprocms']['s2']['todaynew'] eq 1"}
<li class="swiper-slide navbar-item {if condition="$maccms.aid eq 9999"}active{/if}">
<a href="{:mac_url('label/new')}" class="links">
{if condition="$maccms.aid eq 9999"}<div class="current"></div>{/if}
<i class="icon-arrow-go"></i><i class="icon icon-update-o"></i><span>更新</span><small>{:mac_data_count(0,'today','vod')}</small>
</a>
</li>
{/if}
{if condition="$mxprost['mxprocms']['s2']['app'] eq 1"}
<li class="swiper-slide navbar-item {if condition="$maccms.aid eq 9997"}active{/if}">
<a href="{$mxprost.mxprocms.s2.appurl}" class="links" {if condition="$mxprost['mxprocms']['s2']['target'] eq 1"}target="_blank"{/if}>
<i class="icon-arrow-go"></i><i class="icon {$mxprost.mxprocms.s2.appicon}"></i><span>{$mxprost.mxprocms.s2.appname}</span>
</a>
</li>
{/if}
</ul>
</div>
<div class="side-op">
<div class="header-op-list">
<div class="drop">
<div class="header-op-list-btn header-op-history"><i class="icon icon-history-o"></i><span>观看记录</span></div>
<div class="drop-content drop-history">
<div class="drop-content-box">
<ul class="drop-content-items historical">
<li class="drop-item drop-item-title"><i class="icon icon-history"></i><strong>我的观影记录</strong></li>
</ul>
</div>
</div>
<div class="shortcuts-mobile-overlay"></div>
</div>
<div class="header-op-list-btn header-op-search"><i class="icon icon-search"></i></div>
<!-- 侧边用户区:异步碎片占位(由 ajax/userInfo 注入) -->
<div id="user-info-side" class="user-info-skeleton" aria-live="polite"></div>
</div>
</div>
</div>
<!-- ============ 用户碎片注入(顶部 & 侧边) ============ -->
<style>
/* ① 加载骨架(保留原有) */
.user-info-skeleton{min-height:40px;display:flex;align-items:center}
.user-info-skeleton::before{content:'';width:40px;height:40px;border-radius:50%;opacity:.08;background:currentColor;display:inline-block;margin-right:8px}
/* ② PC/大屏:文字按钮;隐藏头像 */
@media (min-width: 560px){
.login-text{ display:inline-flex !important; }
.login-avatar{ display:none !important; }
}
/* ③ 移动端(≤559px):只显示头像,且头像在圆形底中严格居中 */
@media (max-width: 559px){
.login-text{ display:none !important; }
/* 头像容器(圆形底色) */
.login-avatar{
display:inline-flex !important;
width:36px; height:36px; /* 与头像/骨架一致 */
border-radius:50%;
background:#0052D9; /* 主题底色,按需替换 */
position:relative; /* 为绝对定位的图标提供定位上下文 */
/* 去掉任何按钮/背景遗留样式 */
padding:0 !important; margin:0 !important;
border:0 !important; box-shadow:none !important;
vertical-align:middle; /* 防止被基线影响垂直对齐 */
}
/* 字体图标绝对居中(不受字体上/下内边距影响) */
.login-avatar i{
position:absolute; top:50%; left:50%;
transform:translate(-50%, -50%); /* 核心:严格几何居中 */
display:block; line-height:1;
}
/* 父容器若有按钮底色,这里清理 */
.mac_user{
background:none !important;
border:0 !important; box-shadow:none !important;
}
}
</style>
<script>
(function(){
/** 打开登录弹窗:优先 MacCMS 内置,再用站点自定义,最后跳转 */
function openLogin(){
try { if (window.MAC && MAC.User && typeof MAC.User.Login === 'function') { MAC.User.Login(); return; } } catch(e){}
if (typeof window.showLoginPopup === 'function') { window.showLoginPopup(); return; }
window.location.href = "{:mac_url('user/login')}";
}
/** 事件委托:异步插入的元素也可点击 */
$(document).off('click.userlogin', '.login-avatar, .header-op-user, .mac_user')
.on('click.userlogin', '.login-avatar, .header-op-user, .mac_user', function(e){
e.preventDefault();
openLogin();
});
/** 构造“未登录”UI:PC=文字,移动=头像(头像不带 .header-op-user,避免按钮样式) */
function buildLoginHTML(){
return '' +
'<div class="mac_user">' +
'<div class="header-op-user login-text" title="会员中心">登录</div>' +
'<div class="login-avatar" aria-label="登录">' +
'<i class="icon icon-yonghu-o icon20"></i>' +
'</div>' +
'</div>';
}
/** 规范化后端只返回“文字按钮”的情况:补齐头像版,保证小屏只显示头像 */
function normalizeLoginUI($root){
if ($root.find('.member_group').length) return; // 已登录不处理
var hasAvatar = $root.find('.login-avatar').length > 0;
var hasText = $root.find('.login-text').length > 0;
if (!hasAvatar || ($root.find('.header-op-user').length === 1 && hasText)) {
$root.html(buildLoginHTML());
}
}
/** 注入碎片:成功/失败均移除骨架类,且保证未登录时小屏只显示头像 */
function injectUserInfo(sel){
$.get("{:url('ajax/userInfo')}")
.done(function(html){
var $box = $(sel).removeClass('user-info-skeleton').html(html);
normalizeLoginUI($box);
})
.fail(function(){
$(sel).removeClass('user-info-skeleton').html(buildLoginHTML());
});
}
$(function(){
injectUserInfo('#user-info-top');
injectUserInfo('#user-info-side');
});
})();
</script>
<!-- ============ 主题为“1”时的移动端行为(保留你原逻辑) ============ -->
{if condition="$mxprost['mxprocms']['s2']['navtheme'] eq 1"}
<style>
@media (max-width: 559px){
.header { padding: 20px 15px 20px; height: auto; }
.homepage:after { background: none; }
.homepage .side-op { right: 0; }
}
</style>
<script>
$(".header-op-search").click(function () {
$(".search-box").toggleClass("nonenav");
});
$('.cancel-btn').click(function() {
$(".search-box").addClass("nonenav");
});
$(document).scroll(function() {
var H = $(document).scrollTop();
if (H > 20) { $(".sidebar").addClass("sidebar-bg"); }
else { $(".sidebar").removeClass("sidebar-bg"); }
});
$(document).click(function (e) {
if (($(e.target).closest(".search-input").length == 0 || $(e.target).closest(".cancel-btn").length != 0) && $(e.target).closest(".header-op-search").length == 0) {
$(".search-box").addClass("nonenav");
}
});
</script>
{if condition="$maccms.user_status eq 1"}<style>.homepage .side-op .header-op-search{display:block;}</style>{/if}
{/if}
<!-- ============ 主题为“0”时的行为(保留你原逻辑) ============ -->
{if condition="$mxprost['mxprocms']['s2']['navtheme'] eq 0"}
<script>
$(document).scroll(function() {
var H = $(document).scrollTop();
if (H > 20) { $(".sidebar").addClass("sidebar-bg"); }
else { $(".sidebar").removeClass("sidebar-bg"); }
if (H > 140) {
$(".navbar").addClass("open");
$(".side-op").addClass("open");
} else {
$(".navbar").removeClass("open");
$(".side-op").removeClass("open");
}
});
</script>
{if condition="$maccms.user_status eq 1"}<style>.homepage .side-op .header-op-search{display:none;}</style>{/if}
<style>
@media (max-width: 559px){
.homepage .header{{$mxprost.mxprocms.s2.diyheaddm}}
.homepage .sidebar{{$mxprost.mxprocms.s2.diy2headdm}}
}
</style>
{/if}
在核心整页缓存处排除用户信息碎片(强烈建议)
MacCMS 的整页缓存是在
application/common/controller/All.php
里完成的。为避免误把碎片请求也整页缓存,建议对 读/写缓存都做排除。
文件:application/common/controller/All.php
在 load_page_cache($tpl, $type='html')
和 label_fetch($tpl, $loadcache=1, $type='html')
里,构造缓存键前增加:
// —— 排除不应整页缓存的模板(用户碎片 + 用户中心页等) ——
$tpl_excludes = [
'public/user_info', // 本次新增的碎片模板
'user/index', 'user/info', 'user/favs', 'user/upgrade'
];
// $tpl 一般是如 'public/user_info' 这样的模板相对路径
if (in_array($tpl, $tpl_excludes)) {
// 直接返回,不走整页缓存
return;
}
这一步不是“必做”,但强烈建议做:否则某些情况下碎片也会被落地缓存,导致“用户 A 的菜单被用户 B 看见”。
服务器/边缘缓存的同步设置(避免串号)
Nginx配置(示例)
# Ajax 碎片接口:彻底不缓存
location ~* ^/(index\.php/)?ajax/userInfo$ {
add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0, private" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
add_header Vary "Cookie, Accept" always;
}
上述文件/位置按步骤完成后,登录状态不统一的问题就会消失。
快速测试清单
- 清页面缓存(后台:操作→清理管理),刷新首页:
- 未登录:顶部与侧边出现“登录”按钮,点击能打开登录弹窗;
- 登录后:不刷新也会看到两处用户区块变为头像+菜单(碎片每次拉取)。
- 打开若干不同页(分类、详情、专题、自定义页):
- 都应在 300–600ms 内完成用户块更新(取决于接口 RTT)。
- 退出登录:任意页面刷新后,两处都回到“登录”按钮。
- 打开开发者工具 → Network:
- 看到
/ajax/userInfo
每页均请求一次,返回 200,响应头含Cache-Control: no-store
与Vary: Cookie
。
- 看到
用户通过豆瓣id精确搜索影片有方法实现吗,比如某些关键词如 "小姐" 搜索会有很多匹配结果,可能用户要的结果很靠后,如果能通过豆瓣id搜索则可以精确匹配结果
完全可以实现,给豆瓣 ID 字段建立索引,前端识别关键字后,后端若命中多条则展示搜索结果页、命中一条则直接跳详情页。这样就能绕开模糊搜索,做到 ID 级精确匹配,等节后上班了贴一下详细代码。