{mtitle title="部署"/} 以下是通过 Docker Compose 在飞牛 OS 上部署苹果 CMS V10 的完整流程：🔧 1. 准备工作环境要求：飞牛 OS 需已启用 Docker 服务（飞牛 OS 内置 Docker 支持，无需额外配置）。确保存储路径可用（如 "/vol1/docker/MacCms"），用于持久化数据。📝 2. 编写 docker-compose.yml 文件创建以下配置文件（以提供的方案为基础优化）：version: '3.8' services: mysql: image: mysql:5.7 container_name: maccms_mysql restart: always environment: MYSQL_ROOT_PASSWORD: "123456" # 数据库root密码 MYSQL_DATABASE: maccms # 初始数据库名 volumes: - ./data/mysql:/var/lib/mysql # MySQL数据持久化 ports: - "3306:3306" # 数据库端口映射 maccms: image: zhixinfang/maccms:latest # 定制苹果CMS镜像 container_name: maccms_web restart: always ports: - "8054:80" # Web访问端口 volumes: - ./data/html:/var/www/html # 程序文件持久化 depends_on: - mysql # 依赖MySQL服务⚙️ 3. 启动服务部署完成后自动启动服务。🌐 4. 安装苹果 CMS访问安装页面：浏览器打开 "http://飞牛OS的IP:8054"，进入安装向导。填写数据库信息：服务器地址：maccms_mysql（使用Docker容器名，非localhost）数据库名：maccms账号：root密码：123456覆盖数据库：选择“覆盖”（首次安装必选）设置管理员账号：自定义管理员用户名和密码。🎨 5. 配置模板与权限模板安装：下载模板文件（如官方提供的模板包），解压至本地映射目录：./data/html/template/后台设置路径：系统 → 网站参数配置 → 更换“网站模板”和“手机模板”。权限修复（若模板无法访问）：chown -R root:root /vol1/docker/MacCms/data/html/template # 替换为你的实际路径{cloud title="模板以及采集地址" type="lz" url="https://xiaoxu.lanzoub.com/iAWHc2zg1yhg?webtp=VGUCYlszAGUCbQRgAWdQYVIxVGdQc1RkVWZTZ1c9VmFUZVAwAG9QMlN5B2BQM1NnAHQHNgBxAzMHMVtlUyFWZlRiAmJbegBiAmUEZAFjUGZSalRpUGlUb1UxUzFXOVYxVGBQMABpUGVTPQcyUDxTYAA_bBzAAOQNiB2RbaVNsVmVUNwIxW2QAKAJk?w1" password=""/}🔄 6. 采集资源与后续管理资源采集：后台进入 采集 → 自定义资源站，添加采集接口。后台访问：默认后台地址为 http://IP:8054/zhixinfang.php（非admin.php）。⚠️ 常见问题解决问题现象 解决方案安装后提示 File not found 使用正确后台地址 http://IP:8054/zhixinfang.php数据库连接失败 检查容器名maccms_mysql是否一致，确认MySQL容器正常运行（docker logs maccms_mysql）模板无法显示 执行chown修复目录权限，或检查模板文件路径层级💎 总结通过 Docker Compose 部署苹果 CMS V10，飞牛 OS 提供了便捷的容器管理能力，几分钟即可完成影视站搭建。关键点包括：持久化路径映射（MySQL数据、CMS程序）；数据库连接使用容器名而非localhost；后台地址需使用定制路径zhixinfang.php。如需模板或采集接口资源，可参考开源社区（如提供的网盘链接）。此方案适合轻量级影视站需求，结合飞牛 OS 的 Docker 管理界面，后续更新和维护更为简便。
前言AList被收购之后，我前几天就停掉了它。但是这几天发现弄文件实在有点不习惯，于是今天想着找找有没有可以替代的项目。找了之后发现还真有，果然大神还是有的！项目地址是：https://github.com/OpenListTeam/openlist这个项目的文档页面是在：https://openlist.team/zh/guide/install/script.html部署好了之后，除了感觉图标不太一样之外，好像其他操作都一样。挂载网盘的方式跟之前使用AList的时候也是一样的，省去了熟悉新软件使用方法的过程，后台管理页面好像也是一样的：开始部署打开进入到飞牛NAS页面打开【Docker】-【Compose】点击【新增项目】项目名称，必须是英文和数字点击路径选择在【docker】文件夹下新建一个文件夹名为【openlist】比较好辨认，再点击【确定】8.这里选择【创建docker-compose.yml】，复制下面内容粘贴过去services: openlist: image: ghcr.io/openlistteam/openlist-git:beta-aio container_name: openlist volumes: - ./openlist:/opt/openlist/data ports: - '5244:5244' environment: - PUID=0 - PGID=0 - UMASK=022 restart: unless-stopped勾选【创建项目后立即启动】就会自动拉取镜像如果到了这一步遇到【ERROR】，则到这个教程最后找解决方法。等待一会儿就完成了，如果无法拉取镜像，则需要更改DNS尝试，这里更改镜像源是无用的（毕竟已经确定了镜像来源出现【Started】和【Exited：0】的时候，说明已经开始了点击【容器】，找到【openlist】容器，点击【...】-【运行日志】找到这个页面标注着【password】的那一行，冒号[：] 后面就是第一次进入页面的临时密码，把它复制一下打开一个新的浏览器页面，输入【飞牛IP:5244】，我这里是【192.168.33.155:5244】，回车就会到达这个页面，账户输入【admin】，密码填写刚刚复制的那个临时密码。最后点击【登录】就能进入第一次进入，页面很空，什么都没有。需要点击页面正下方的【管理】首先需要更改一下密码！弄好这一切之后就可以挂载各类网盘了，支持的类型很多。（请自行查看）{lamp/}
http { server_tokens off; # 隐藏版本号 # 想要彻底清除 Server 响应头可以选择 ngx_headers_more 模块，这个模块需要额外安装。或者直接安装 nginx-extras 包也会带上这个模块。 more_clear_headers Server; # 清除 Server 响应头 # 阻止ip访问 server { # default_server 是一个配置参数，用于指定当请求的域名（Host 头）没有匹配任何 server 块时，Nginx 应该使用哪个 server 块来处理这些请求。 listen 443 ssl default_server; server_name _; # 匹配所有未配置的域名或IP，这样设置了 default_server 的 server 块（即本块）就会处理所有未匹配的请求。 ssl_certificate 证书.pem; # 签名证书路径，相对路径、绝对路径都可以 ssl_certificate_key 私钥.key; # 私钥路经 return 403; # 返回禁止访问 } # https配置模板 server { # https的默认端口是443，只有把端口写成443，登录网站的时候才不需要写端口号 listen 443 ssl; # [::] 是 IPv6 的通配符地址，表示监听所有可用的 IPv6 地址（类似于 IPv4 的 0.0.0.0） listen [::]:443 ssl; # Nginx 会先检查请求的域名是否与某个 server_name 精确匹配， # 如果没有精确匹配，会尝试使用通配符匹配。如果没有找到匹配的 server_name，Nginx 会将请求交给 default_server 的 server 块。 server_name 这里写你的域名; # ssl配置，证书可以使用免费的Let's Encrypt生成，阿里云上也有，但是签名需要域名， # 不需要域名的可以自己使用 openssl 创建自签名证书，但是自签名证书不受信任 ssl_certificate 证书.pem; # 签名证书路径，nginx支持pem格式的证书 ssl_certificate_key 私钥.key; # 私钥路经 # 启用ssl协议，强制使用 TLS 1.2 和 1.3 版本 ssl_protocols TLSv1.2 TLSv1.3; # 启用加密套件，加密套件的顺序用冒号分隔。很重要，Nginx 会按照这个顺序选择一个加密套件 ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'; # 强制性要求浏览器在加密连接时优先使用 服务器端 的加密套件 ssl_prefer_server_ciphers on; # 会话缓存：会话缓存能够存储客户端和服务器之间建立的 SSL/TLS 会话，避免每次请求都重新建立完整的握手过程，从而提升性能，减少 CPU 使用。通常 1 MB 可以存储大约 4000 个会话。 ssl_session_cache shared:SSL:1m; # 会话缓存时间：用于设置 SSL 会话缓存中会话的过期时间。在指定时间内，客户端可以重用之前缓存的会话，对于大多数网站，设置为 5m（5 分钟）到 10m（10 分钟）是合理的范围。 ssl_session_timeout 5m; # 向 HTTP 响应中添加自定义的 HTTP 头部信息。语法：add_header <header_name> <value> [always]; # add_header 指令可以写在 http、server 或 location 块中，作用范围会逐级缩小。如果多个级别的 add_header 冲突，优先使用最具体（内层）的配置。 # 强制客户端使用 HTTPS 访问，防止中间人攻击。 # max-age=31536000：缓存 1 年。includeSubDomains：对子域生效。preload：允许加入浏览器 HSTS 预加载列表。 # [always]: 默认情况下，add_header 只会在响应状态码为 200、204、206、301、302、303 或 304 时添加。如果加上 always 参数，头部会在所有响应中添加（包括错误状态码，如 404 或 500）。 add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # 只允许特定的 HTTP 方法。 add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; # 限制加载资源的来源，防止恶意脚本注入。 # default-src 'self' 仅允许加载本域的资源。如果你不熟悉域的概念就别用 # script-src 'self' 'unsafe-inline' 限制脚本加载来源，仅允许本域和内联脚本。如果你不熟悉域的概念就别用 # img-src 'self' data: 限制图片来源，仅允许本域和 data: 协议。 add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; img-src 'self' data:;"; # 启用 gzip 压缩，优化传输速度和网络带宽使用。 gzip on; # 文件大于 1KB 才压缩，对于小文件（小于 1KB 的资源），压缩的效果不明显，还可能导致处理开销增加，因此跳过。 gzip_min_length 1024; # 设置压缩等级为 5，这是一种性能和压缩比的折中选择。Gzip 压缩需要消耗服务器的 CPU 资源，过高的压缩等级可能增加服务器的负载。推荐压缩等级为 4~6。 gzip_comp_level 5; # 指定要压缩的资源类型：text/plain: 纯文本文件。text/css: CSS 样式表文件。application/json: JSON 数据（比如 API 响应）。 # application/javascript: JavaScript 文件。text/xml 和 application/xml: XML 文件。image/svg+xml: 矢量图片（SVG 格式）。 # 图片文件通常不需要 Gzip 压缩。他们已经是高度压缩了。 gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml; # 添加 Vary: Accept-Encoding 头部用于通知缓存服务器根据客户端是否支持 Gzip 压缩返回不同的内容。 gzip_vary on; # 为所有代理请求启用压缩，适用于反向代理场景。 gzip_proxied any; # location 指令用于定义一个位置块，它根据请求的 URI 匹配特定的路径。location也可以嵌套在其他location块中。 # 前端代理，根路径 / 指向静态文件 index/index.html。 location / { # 指定静态文件的根目录 root html/course; # 指定当请求为url目录时，默认返回的页面 index index/index.html; } # 后端代理，将 /api/ 开头的请求转发到后端服务器的 http://localhost:5000 上 location /api/ { # proxy_pass 指定代理的后端服务器的地址。 # 注意：http://localhost:5000/ 和 http://localhost:5000 所代表的url拼接规则是不一样的。自己去搜 proxy_pass 参数怎么用 proxy_pass http://localhost:5000/; # proxy_set_header 用于设置转发给后端服务器的请求头部。 # Host头部保持了原始请求的Host值。 proxy_set_header Host $host; # X-Real-IP头部记录了原始客户端的IP地址。 proxy_set_header X-Real-IP $remote_addr; # X-Forwarded-For头部是一个逗号分隔的列表，包含了从客户端到代理服务器的所有IP地址（包括客户端的IP地址）。 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # X-Forwarded-Proto头部记录了原始请求使用的协议（如http或https）。 proxy_set_header X-Forwarded-Proto $scheme; } # WebSocket 代理，我使用的是 flask_socketio，请根据实际情况修改匹配的url location /socket.io/ { proxy_pass http://localhost:5000; # 这里必须使用 http1.1 版本，因为1.1版本才是长连接 proxy_http_version 1.1; # 设置 Upgrade 请求头，用于处理协议升级（从 HTTP 升级到 WebSocket）。 proxy_set_header Upgrade $http_upgrade; # 强制将 Connection 设置为 "upgrade"，以支持协议切换。 proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 设置超时时间为 60 秒，用于保持 WebSocket 连接。 proxy_read_timeout 60s; } # 单独按文件类型配置静态资源，可以单独为图片、CSS、JS 文件设置不同的缓存策略。 # 也可以写到某页的 location 块中。比如： # location / { # root html/course; # index index/index.html; # expires 30d; # add_header Cache-Control "public, max-age=2592000"; # } # 这里我选择统一缓存网站的所有静态资源，使用正则表达式匹配文件后缀。每页的 location 块中也可以单独配置，如上。 # 配置图片、视频缓存 location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|mp4)$ { # 必须设置 root 参数，指定静态文件的根目录。不然无法找到静态文件。 root html/course; # 设置静态资源的缓存时间为 30 天。 expires 30d; # 设置浏览器缓存策略。public：表示该资源可以被任何缓存（包括浏览器缓存和中间代理缓存）存储和共享。max-age=2592000 表示缓存时间为 30 天。 # 浏览器在 30 天内不会重新请求资源，直接从本地缓存加载。 add_header Cache-Control "public, max-age=2592000"; } # 配置 CSS 和 JS 文件缓存 location ~* \.(css|js)$ { root html/course; expires 1d; # 缓存 1 天 add_header Cache-Control "public, max-age=86400"; } # 页面资源（HTML 文件） location ~* \.(html)$ { root html/course; add_header Cache-Control "no-cache, no-store, must-revalidate"; # 确保内容实时更新 } # 防止访问隐藏文件（如 .git 文件） location ~ /\. { deny all; } # 404错误页面配置：当请求的文件不存在时，转入 /404.html 页面 error_page 404 /404.html; # 匹配 /404 错误页面，=表示精确匹配 location = /404.html { # 指定 404.html 页面的位置 root /usr/share/nginx/html/404.html; } } }
1.修改functions.php文件，底部增加，文件路径：/usr/themes/Joe// 文章阅读时长设置 $onlineTime = new Typecho_Widget_Helper_Form_Element_Select( 'onlineTime', array( 'off' => '关闭（默认）', 'on' => '开启', ), 'on', '是否启用文章阅读时长统计', '介绍：开启后，文章底部展示文章字数，预计阅读时长和已阅读时长' ); $onlineTime->setAttribute('class', 'joe_content joe_custom'); //如果设置无法展示，请将joe_custom替换为joe_other $form->addInput($onlineTime->multiMode());2.修改article.php文件，文件路径：usr/themes/Joe/public第一段代码<div class="contain" style="margin-bottom: 10px; <?php if(Helper::options()->onlineTime !== 'on') echo 'display:none;' ?>"> 本文共 <?php art_count($this->cid); ?> 个字数,平均阅读时长 ≈ <?php echo art_time($this->cid); ?>分钟 第二段代码<?php //文章阅读时间统计 function art_time ($cid){ $db=Typecho_Db::get (); $rs=$db->fetchRow ($db->select ('table.contents.text')->from ('table.contents')->where ('table.contents.cid=?',$cid)->order ('table.contents.cid',Typecho_Db::SORT_ASC)->limit (1)); $text = preg_replace("/[^\x{4e00}-\x{9fa5}]/u", "", $rs['text']); $text_word = mb_strlen($text,'utf-8'); echo ceil($text_word / 400); } //文章字数统计 function art_count ($cid){ $db=Typecho_Db::get (); $rs=$db->fetchRow ($db->select ('table.contents.text')->from ('table.contents')->where ('table.contents.cid=?',$cid)->order ('table.contents.cid',Typecho_Db::SORT_ASC)->limit (1)); $text = preg_replace("/[^\x{4e00}-\x{9fa5}]/u", "", $rs['text']); echo mb_strlen($text,'UTF-8'); } ?>
1.修改教程，路径/Joe/public/header.php修改首页，官版327行。 首页2.修改栏目，用下面代码替换 栏目 栏目3.页面 页面4.推荐 推荐
成品如图1.修改functions.php，底部新增，文件路径：usr/themes/Joe；如果无法显示设置，则将以下joe_custom修改为joe_other// 在线人数设置 $onlinePersion = new Typecho_Widget_Helper_Form_Element_Select( 'onlinePersion', array( 'on' => '开启（默认）', 'off' => '关闭', ), 'on', '是否启用在线人数统计', '介绍：开启后，文章底部展示当前在线人数' ); $onlinePersion->setAttribute('class', 'joe_content joe_custom'); $form->addInput($onlinePersion->multiMode());2.修改footer.php文件，文件路径：/usr/themes/Joe/public①第一段，顶部新增<?php //首先你要有读写文件的权限，首次访问不显示，正常情况刷新即可 $online_log = "slzxrs.dat"; //保存人数的文件到根目录, $timeout = 30;//30秒内没动作者,认为掉线 $entries = file($online_log); $temp = array(); for ($i=0;$i<count($entries);$i++){ $entry = explode(",",trim($entries[$i])); if(($entry[0] != getenv('REMOTE_ADDR')) && ($entry[1] > time())) { array_push($temp,$entry[0].",".$entry[1]."\n"); //取出其他浏览者的信息,并去掉超时者,保存进$temp } } array_push($temp,getenv('REMOTE_ADDR').",".(time() + ($timeout))."\n"); //更新浏览者的时间 $slzxrs = count($temp); //计算在线人数 $entries = implode("",$temp); //写入文件 $fp = fopen($online_log,"w"); flock($fp,LOCK_EX); //flock() 不能在NFS以及其他的一些网络文件系统中正常工作 fputs($fp,$entries); flock($fp,LOCK_UN); fclose($fp); $tj= "在线人数：".$slzxrs."人"; ?>②第二段，中间新增<div class="item" style="margin: 10px; <?php if(Helper::options()->onlinePersion !== 'on') echo 'display:none;' ?>"> <?php echo $tj ?>
歌单播放{music-list id="8904187604" color="#1989fa" autoplay="autoplay"/}单曲播放{music id="192251" color="#00ffff" autoplay="autoplay"/}音乐搜索音乐地址 作者原地址失效了，所以替换播放插件地址就行了。，主要替换两个地方这是歌单 /usr/themes/Joe/assets/js/joe.short.min.js这是单曲 /usr/themes/Joe/assets/js/joe.short.js修改方法是用：https://www.nmssb.cn/usr/themes/Joe/163musicapi.php?id=(借用笔记堡地址)替换掉https://www.vvhan.com/usr/themes/Joe/NeteaseCloudMusicApi.php?id=(原作者地址)就好了也可以把附件里面的NeteaseCloudMusicApi.php，放在自己网站，然后修改地址为自己的网站地址。{cloud title="音乐文件" type="lz" url="https://hndy8.lanzoub.com/iPprk2y4komh" password="77mu"/}