Framadate 1.1.19 运维与升级手册

Framadate 1.1.19 生产运维实录:Apache+MariaDB 形态下的日常巡检、mysqldump 备份恢复(实测 7310 字节/5 表全量校验)、日志监控、过期清理机制、迁移幂等升级与回滚、数据库停机降级表现(302 maintenance.php)等全链路操作均附实测回显。

FramadateFramadate自托管PHPMariaDB2026/11/01

Framadate 1.1.19 运维与升级手册

目标:给出一台已按标准方式部署的 Framadate 1.1.19 服务器(Ubuntu 22.04.5 + Apache 2.4.52 + PHP 8.1.2 + MariaDB 10.6.23,监听 8145 端口)的日常运维完整操作手册——巡检、备份恢复、监控告警、清理机制、升级回滚与故障处置,全部命令与回显取自真实运行环境。安装过程见 《Ubuntu 22.04 安装 Framadate 1.1.19 指南》。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS(x86_64)
  • Web 服务器:Apache 2.4.52,Listen 8145,vhost /etc/apache2/sites-available/framadate.conf
  • PHP:8.1.2-1ubuntu2.28(mod_php)
  • 数据库:MariaDB 10.6.23,库名 framadate,账号 framadate(密码 q1w2e3r4),表前缀 fd_
  • 应用目录:/var/www/framadate(属主 www-data:www-data)
  • 应用版本:const VERSION = '1.1.19';(app/inc/constants.php 第 21 行实测)
  • 数据现状:1 张日期投票(公开 ID dvdEakcXnbWf7eXf,截止 2026-11-14)、2 条投票记录、5 张业务表

二、日常巡检

2.1 服务与端口状态

巡检第一步确认两个 systemd 服务与监听端口都活着:

systemctl is-active apache2
systemctl is-active mariadb
ss -ltn | grep 8145
curl -s -o /dev/null -w "首页 HTTP %{http_code}\n" http://localhost:8145/
active
active
LISTEN 0      511          0.0.0.0:8145      0.0.0.0:*    users:(("apache2",pid=15383,fd=4),("apache2",pid=15388,fd=4))
首页 HTTP 200

(ss 回显节选:apache2 主进程 + 5 个工作进程共用监听 fd,此处展示前两个。)任何一环非 active/非 200,直接进入第七章故障处置。

2.2 应用体检页 admin/check.php

Framadate 自带安装体检页,运行期同样可用:它逐项校验 PHP 版本(对照 PHP_NEEDED_VERSION = '7.3')、intl/mbstring/openssl 扩展与数据库连通性。实测:

curl -s -o /dev/null -w "check.php %{http_code}\n" http://localhost:8145/admin/check.php
curl -s http://localhost:8145/admin/check.php | grep -oE "<title>[^<]*" | head -1
check.php 200
<title>Installation checking

页面为全绿即环境健康;若换 PHP 大版本或裁剪扩展后出现红色条目,按提示补装对应 php-* 包。

2.3 数据面巡检:管理工具页与库表

admin/ 目录是内置运维工具箱(实测全部 200):

curl -s -o /dev/null -w "admin/polls.php %{http_code}\n" http://localhost:8145/admin/polls.php
curl -s -o /dev/null -w "admin/logs.php %{http_code}\n" http://localhost:8145/admin/logs.php
curl -s -o /dev/null -w "admin/purge.php %{http_code}\n" http://localhost:8145/admin/purge.php
admin/polls.php 200
admin/logs.php 200
admin/purge.php 200
  • admin/polls.php:列出站内全部投票(标题、创建人、邮箱、截止日、公开 ID),实测回显:
<td>Reunion du projet</td>
<td>Admin</td>
<td>admin&commat;framadate&period;local</td>
<td>14/11/26</td>
<td>dvdEakcXnbWf7eXf</td>
  • admin/logs.php:在线查看应用日志(即 2.4 节 stdout.log);
  • admin/purge.php:过期投票清理页(机制见第三章 3.4 节);
  • 根目录 find_polls.php:面向用户的「找回我的投票」页,凭管理员邮箱收链接,实测 200,标题 Où sont mes sondages ?。

库表级巡检(行数与迁移记录):

mysql -uframadate -pq1w2e3r4 framadate -e \
  "SELECT (SELECT COUNT(*) FROM fd_poll) polls,
          (SELECT COUNT(*) FROM fd_slot) slots,
          (SELECT COUNT(*) FROM fd_vote) votes,
          (SELECT COUNT(*) FROM fd_framadate_migration) migrations;"
polls  slots  votes  migrations
1      3      2      13

migrations 恒为 13(1.1.19 全部迁移数),若某日变少说明迁移表被误动,需立即排查。

2.4 应用日志 stdout.log

配置项 LOG_FILE = 'admin/stdout.log' 决定应用事件日志位置(相对应用根目录),实测全文:

20261101 152043 [CREATE_POLL] id:tXeTu4G6InJoAHGd, title: Reunion du projet, format:D, admin:Admin, mail:admin@framadate.local
20261101 152114 [CREATE_POLL] id:dvdEakcXnbWf7eXf, title: Reunion du projet, format:D, admin:Admin, mail:admin@framadate.local

格式为「时间戳 [事件] 键值对」。事件类型实测两类:CREATE_POLL(建票)与 EXPIRATION(清理动作,见 3.4 节)。注意第一条 tXeTu4G6InJoAHGd 即安装期 SMTP 500 事故留下的孤儿票日志——票已从 fd_poll 删除,日志仍留痕,属正常现象。该文件随事件增长,纳入日志轮转(见 4.1 节)。

三、备份与恢复

3.1 数据库备份(mysqldump)

Framadate 的全部状态都在 MariaDB 的 5 张表里,mysqldump 即完整业务备份。实测:

mysqldump -uframadate -pq1w2e3r4 --single-transaction framadate > /root/framadate-backup-2026-11-01.sql
ls -l /root/framadate-backup-2026-11-01.sql
grep -c "INSERT INTO" /root/framadate-backup-2026-11-01.sql
grep -E "Table structure" /root/framadate-backup-2026-11-01.sql
-rw-r--r-- 1 root root 7310 Nov  1 14:38 /root/framadate-backup-2026-11-01.sql
4
-- Table structure for table `fd_comment`
-- Table structure for table `fd_framadate_migration`
-- Table structure for table `fd_poll`
-- Table structure for table `fd_slot`
-- Table structure for table `fd_vote`

要点:--single-transaction 对 InnoDB 表做一致性快照、不锁表;备份含 5 表结构与 4 组 INSERT(当前数据量)。生产建议每日备份并保留 7 天,实测写入 crontab:

(crontab -l 2>/dev/null; echo "17 3 * * * mysqldump -uframadate -pq1w2e3r4 --single-transaction framadate | gzip > /var/backups/framadate-\$(date +\%F).sql.gz") | crontab -
crontab -l | tail -1
systemctl is-active cron
17 3 * * * mysqldump -uframadate -pq1w2e3r4 --single-transaction framadate | gzip > /var/backups/framadate-$(date +%F).sql.gz
active

⚠️ 注意:cron 行里的口令是演示值;生产环境应改用 mysql_config_editor 登录路径或 /etc/mysql/debian.cnf 委托 root 身份,避免口令进 crontab。

3.2 恢复演练

恢复到独立库验证备份可用性(不碰生产库),实测:

mysql -uroot -e "CREATE DATABASE IF NOT EXISTS framadate_restore;"
mysql -uroot framadate_restore < /root/framadate-backup-2026-11-01.sql
mysql -uroot framadate_restore -e "SHOW TABLES; SELECT id,title FROM fd_poll;"
Tables_in_framadate_restore
fd_comment
fd_framadate_migration
fd_poll
fd_slot
fd_vote
id              title
dvdEakcXnbWf7eXf  Reunion du projet

5 表齐全、投票行完整即备份合格。生产整体回滚时把导入目标换成 framadate 库即可;迁移记录表随备份一起恢复,恢复后 admin/migration.php 重跑仍显示全部 Passé,不会二次迁移。

3.3 代码与配置备份

代码目录(含 vendor/ 依赖)整体打包即应用侧备份,实测打包产物 3.6M:

cd /var/www
tar czf /root/framadate-code-$(date +%F).tar.gz --exclude="framadate/app/inc/config.php" framadate
ls -lh /root/framadate-code-*.tar.gz
-rw-r--r-- 1 root root 3.6M Nov  1 14:38 /root/framadate-code-2026-11-01.tar.gz

app/inc/config.php 含数据库口令,单独走加密渠道保存、不进公共备份包;tpl_c/ 编译缓存可再生,无需备份。

3.4 过期清理机制(purge)

Framadate 不自动删票,需运维触发:admin/purge.php 页面提交后调用 PurgeService::purgeOldPolls(),删除「已过截止日且超出宽限期」投票及其 slot/vote/comment 级联数据。源码实测:

// admin/purge.php 触发逻辑
if ($action === 'purge' && $securityService->checkCsrf('admin', $_POST['csrf'])) {
    $count = $purgeService->purgeOldPolls();
    $message = __('Admin', 'Purged:') . ' ' . $count;
}
  • 宽限期由配置 PURGE_DELAY = 60 控制(截止日之后第 61 天起可被清除);
  • 清理动作带 CSRF 校验,浏览器打开 admin/purge.php 点按钮即可;命令行等价做法实测(同会话取令牌再 POST):
cd /tmp
curl -s -c jar http://localhost:8145/admin/purge.php -o p.html
CSRF=$(grep -oE 'name="csrf" value="[^"]+"' p.html | head -1 | sed -E 's/.*value="([^"]+)".*/\1/')
curl -s -b jar http://localhost:8145/admin/purge.php \
  --data-urlencode "action=purge" --data-urlencode "csrf=$CSRF" | grep -oE 'Purg[^<]*'
Purg&eacute;s&nbsp;&colon; 0

返回 Purgés : 0(法语界面下消息按 Purg&eacute;s&nbsp;&colon; 0 渲染):当前唯一活跃票截止 2026-11-14、未到「截止日+60 天」窗口,清理安全空跑。每次执行向 stdout.log 写 [EXPIRATION] Going to purge N poll(s)... 事件行(仅当 N>0)。

💡 说明:对外服务的实例建议把清理做成每日 cron(用上面的取令牌-POST 两步法即可脚本化),避免过期票无限堆积;宽限天数想收紧就改 PURGE_DELAY。

四、监控与告警

4.1 日志源与轮转

三个日志源各司其职:

文件内容来源配置
/var/log/apache2/framadate_access.log访问流水(combined)vhost CustomLog
/var/log/apache2/framadate_error.logPHP Fatal/Warning、Apache 错误vhost ErrorLog
/var/www/framadate/admin/stdout.log应用事件(建票/清理)LOG_FILE 常量

实测错误日志样例(一次 strftime 兼容故障的完整记录):

[Sun Nov 01 14:23:56 2026] [php:error] [pid 14735] [client 127.0.0.1:58184]
PHP Fatal error:  Uncaught TypeError: strftime(): Argument #2 ($timestamp) must be
of type ?int, string given in /var/www/framadate/exportcsv.php:80

stdout.log 不在 Apache 日志目录下,给 logrotate 加一条即可:

cat > /etc/logrotate.d/framadate <<'EOF'
/var/www/framadate/admin/stdout.log {
    weekly
    rotate 8
    compress
    missingok
    notifempty
    create 0644 www-data www-data
}
EOF
logrotate -d /etc/logrotate.d/framadate 2>&1 | head -5
WARNING: logrotate in debug mode does nothing except printing debug messages!  Consider using verbose mode (-v) instead if this is not what you want.

reading config file /etc/logrotate.d/framadate
Reading state from file: /var/lib/logrotate/status
state file /var/lib/logrotate/status does not exist

reading config file 无报错即配置语法合格(-d 为干跑,不真轮转)。

4.2 关键告警规则

最小可用三条(cron 每小时 grep 一次即可起步,实测计数命令):

grep -c "PHP Fatal" /var/log/apache2/framadate_error.log
grep -c "SQLSTATE" /var/log/apache2/framadate_error.log
tail -100 /var/log/apache2/framadate_access.log | grep -cE ' (5[0-9]{2}) '
2
1
0
  • PHP Fatal 增量 > 0:应用级故障(本项目已知两处,见第七章);
  • SQLSTATE 出现:数据库断连,服务已自动降级到维护页(见 6.4 节);
  • 访问日志 5xx 占比突增:优先看 error log 对应时间点。

⚠️ 注意:stdout.log 与 admin/ 目录位于 DocumentRoot 内,默认即可被公网访问(实测 curl http://localhost:8145/admin/stdout.log 返回 200 并吐出日志内容)——日志里的投票人邮箱属敏感信息。上线前务必在 vhost 内封禁:

<FilesMatch "stdout\.log">
    <IfModule mod_authz_core.c>
        Require all denied
    </IfModule>
</FilesMatch>

或直接把 LOG_FILE 指到 DocumentRoot 之外(如 /var/log/framadate/stdout.log,目录需 chown www-data)。

五、扩容缩容

Framadate 是「无状态 PHP 多页应用 + 单库」结构,扩缩容只在两处做文章:

  1. 应用层可横向复制:任意台数服务器共享同一 MariaDB 与同一份代码即可分流;PHP 会话仅用于 CSRF 令牌与语言偏好(响应头实测 Set-Cookie: PHPSESSID=...),文件会话默认落各机 /tmp,多机场景要么前置会话粘滞,要么把 session.save_handler 指到 Redis(改 /etc/php/8.1/apache2/php.ini 后 systemctl reload apache2 即生效)。
  2. 数据库是唯一有状态组件:容量规划即 MariaDB 规划;投票数据行小(当前 1 票 3 槽 2 票记录库仅 7.3KB dump),单实例 MariaDB 足以支撑万级活跃投票,瓶颈出现时优先加索引内存(innodb_buffer_pool_size),再考虑读写分离——应用无只读模式,读写分离需中间件,一般到不了这一步。

💡 说明:官方未提供集群/多主形态,应用层也没有节点注册概念,「扩容」= 加 Web 机 + LB 反代(反代场景记得设 const APP_URL,否则邮件/链接里的域名取自动探测值出错)。

六、升级与回滚

6.1 版本与迁移机制

升级安全性建立在两点:

  • 版本号常量:app/inc/constants.php 实测 const VERSION = '1.1.19';(第 21 行)、const PHP_NEEDED_VERSION = '7.3';(第 24 行);
  • 迁移判重表 fd_framadate_migration:admin/migration.php 内置 14 个迁移类(AddColumn_ValueMax_In_poll_For_1_1 重复登记一次,实际 13 条),凡记录表已有的条目直接跳过。实测重跑:
curl -s -o /dev/null -w "migration重跑 %{http_code}\n" http://localhost:8145/admin/migration.php
mysql -uframadate -pq1w2e3r4 framadate -e "SELECT COUNT(*) FROM fd_framadate_migration;"
migration重跑 200
COUNT(*)
13

页面逐条显示 Passé(已执行跳过),重跑无副作用——这是升级/回滚操作可反复演练的基础。

6.2 升级到新版本步骤

1.1.x 系列(1.1.17/1.1.18/1.1.19)数据库结构完全一致,CHANGELOG.md 实测 1.1.19 仅一行修复(移除邮件 X-Mailer 头),此类版本升级只换代码、不触发新迁移。以「从 1.1.18 升级到 1.1.19」为例的标准五步:

# 1. 备份(第三章命令,缺一不可)
mysqldump -uframadate -pq1w2e3r4 --single-transaction framadate | gzip > /var/backups/pre-upgrade.sql.gz

# 2. 下载新版本源码包并解包
wget https://github.com/framasoft/Framadate/archive/refs/tags/1.1.19.tar.gz -O /tmp/fd.tar.gz
tar xzf /tmp/fd.tar.gz -C /tmp

# 3. 替换代码,保留配置与权限
mv /var/www/framadate /var/www/framadate.old
mv /tmp/framadate-1.1.19 /var/www/framadate
cp /var/www/framadate.old/app/inc/config.php /var/www/framadate/app/inc/config.php
chown -R www-data:www-data /var/www/framadate

# 4. 拉取新版依赖(外网不通时按安装篇走代理 192.168.1.72:7897)
cd /var/www/framadate && composer install --no-dev

# 5. 触发迁移并验证
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8145/admin/migration.php
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8145/

第 5 步两条命令实测均应 200(本机对 1.1.19 执行迁移与首页验证的实测回显:migration重跑 200、首页 HTTP 200);admin/check.php 全绿即升级完成,观察正常后删除 framadate.old。

⚠️ 注意(本地补丁必须重打):项目 2021-12 后停更,PHP 8.1 的两处兼容修复不会随升级带过来——exportcsv.php 第 80 行的 (int) 强转补丁(CSV 导出 500)与 config.php 的 use_smtp => false(建票 500)。升级后先跑一遍 curl "http://localhost:8145/exportcsv.php?poll=dvdEakcXnbWf7eXf",若 500 立即重打补丁:

sed -i 's/strftime(\$format, \$slot->title)/strftime($format, (int) $slot->title)/' \
  /var/www/framadate/exportcsv.php

6.3 回滚

回滚 = 反向执行 6.2:代码目录换回 framadate.old(或解包 3.3 节代码备份),数据库用 pre-upgrade.sql.gz 恢复:

gunzip -c /var/backups/pre-upgrade.sql.gz | mysql -uroot framadate

迁移记录表随库一起回滚,无需单独处理;回滚后验证同 6.2 第 5 步两条 curl。

6.4 维护窗口与自动降级

Framadate 没有主动维护开关,但具备数据库断连自动降级能力,实测停库表现:

systemctl stop mariadb
curl -s -o /dev/null -w "停库后首页: %{http_code} -> %{redirect_url}\n" http://localhost:8145/
curl -sL http://localhost:8145/ | grep -oE "SQLSTATE[^<]*" | head -1
停库后首页: 302 -> http://localhost:8145/maintenance.php
SQLSTATE[HY000] [2002] No such file or directory

停掉 MariaDB 后所有请求被 302 到 maintenance.php(提示稍后再试),不产生 500 雪崩——这正是做数据库维护(如在线 DDL、升级 MariaDB)的推荐姿势:停库即全站优雅降级,维护完 systemctl start mariadb,实测恢复:

systemctl start mariadb
curl -s -o /dev/null -w "恢复后首页: %{http_code}\n" http://localhost:8145/
恢复后首页: 200

七、故障排查手册

按「现象 → 定位 → 处置」组织运维侧高频故障;功能级 bug(投票提交、ICS 导出等)见第九章 FAQ。

7.1 首页 500 白屏

定位顺序:tail -20 /var/log/apache2/framadate_error.log 看 PHP Fatal 具体文件行号。三类常见根因实测:

  • SQLSTATE[HY000] [2002]:MariaDB 未运行或 socket 路径不符 → systemctl start mariadb;
  • Permission denied 写 tpl_c/:Smarty 编译缓存不可写 → chown -R www-data:www-data /var/www/framadate/tpl_c;
  • Failed to open stream: config.php:配置文件丢失/路径错 → 从备份恢复 app/inc/config.php。

7.2 建票最后一步 500(SMTP connect() failed)

错误日志实测原文:

PHP Fatal error:  Uncaught PHPMailer\PHPMailer\Exception: SMTP connect() failed.
in /var/www/framadate/vendor/phpmailer/phpmailer/src/PHPMailer.php:1982
#3 /var/www/framadate/create_date_poll.php(213): Framadate\Services\MailService->send()

处置:sed -i "s/'use_smtp' => true/'use_smtp' => false/" /var/www/framadate/app/inc/config.php;随后清理已写库的孤儿票:

mysql -uframadate -pq1w2e3r4 framadate -e \
  "DELETE FROM fd_slot WHERE poll_id='tXeTu4G6InJoAHGd';
   DELETE FROM fd_poll WHERE id='tXeTu4G6InJoAHGd';"

(孤儿票 ID 从 stdout.log 的 [CREATE_POLL] 行反查。)

7.3 CSV 导出 500(strftime TypeError)

见 6.2 节 ⚠️ 的补丁命令,打完 exportcsv.php?poll=dvdEakcXnbWf7eXf 实测 200、text/csv; charset=utf-8。

7.4 投票短链 404 但查询串正常

.htaccess、AllowOverride All、mod_rewrite 三件套缺一即此症状,逐项核对命令见 《Ubuntu 22.04 安装 Framadate 1.1.19 指南》 9.3 节;改完 systemctl reload apache2 用 curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8145/dvdEakcXnbWf7eXf 复测(实测 200)。

7.5 管理员链接丢失

管理权 = 24 位 admin_id,用户侧丢了可走 find_polls.php 凭邮箱重发(需 use_smtp=true 且 SMTP 可用)。运维侧直接查库兜底(实测):

mysql -uframadate -pq1w2e3r4 framadate -e "SELECT id,admin_id FROM fd_poll WHERE id='dvdEakcXnbWf7eXf';"
id                admin_id
dvdEakcXnbWf7eXf  dvdEakcXnbWf7eXfgPd6E5R4

拼上 /<admin_id>/admin 即管理页地址(干净链接规则 4.7 节已重写路由)。

八、常见问题

8.1 purge 会不会误删活跃投票

不会。purgeOldPolls() 只选「截止日 + PURGE_DELAY(60 天) 已过」的票;实测当前唯一活跃票(截止 2026-11-14)执行清理返回 Purgés : 0(见 3.4 节命令)。宽限期想缩短改 PURGE_DELAY 即可,但建议保持默认——投票人可能迟来管理结果。

8.2 界面默认法语,怎么切英文

DEFAULT_LANGUAGE = 'fr' 是站点主语言;访客侧带 ?lang=en 即切英文,实测首页标题从 Organiser des rendez-vous simplement, librement. - Framadate 变为 Make your polls - Framadate。支持的 9 种语言见配置 $ALLOWED_LANGUAGES(fr/en/oc/es/de/nl/it/br/ca)。

8.3 升级后要不要重新 composer

要。vendor/ 不随 tag 源码包分发(安装篇 4.5 节),换代码目录后必须 composer install --no-dev;framagit.org 直连超时的机器记得先 export 代理。

8.4 数据库账号权限能再收紧吗

可以。应用只需 framadate 库的 DML/DDL(迁移建表需要 DDL),无法只给 DML;但可禁止该账号远程登录(保持 localhost)、并用 REVOKE ALL ON *.* 确认无全局权限——实测 SHOW GRANTS 仅 USAGE ON *.* + framadate.* 库级授权,已是最小形态。

九、参考链接

Framadate 1.1.19 运维与升级手册 - Quzuo